<template>
  <div class="page-wrapper">
    <!-- 引入字体组件 -->
    <font-loader />

    <!-- 页面禁用遮罩：当必需参数缺失时阻止交互并提示 -->
    <div v-if="pageDisabled" class="page-disabled-mask">
      <div class="mask-content">
        <i class="el-icon-warning-outline mask-icon"></i>
        <div class="mask-title">缺少必要参数</div>
        <div class="mask-desc">未获取到 App名称，请通过带有 appName 参数的链接进入本页。</div>
      </div>
    </div>
    <!-- 顶部导航栏 -->
    <nav-bar :is-loggedin="false" :theme-icon="'fas fa-moon'" @toggle-theme="toggleTheme" @login="handleLogin" />

    <div class="home-root">

      <!-- 侧边栏组件 -->
      <chat-sidebar :visible.sync="showHistoryPanel" :conversations="conversations" :current-conversation-id="currentConversationId" default-conversation-name="新对话" toggle-button-title="历史消息" @new-chat="startNewChat"
                    @load-conversation="loadConversation" @delete-chat="deleteChat" />

      <!-- 主内容区 -->
      <div class="main-content" :style="mainContentStyle">
        <!-- 聊天容器 - 包含消息展示和输入框的大框体 -->
        <div class="chat-container">
          <!-- 外层滚动容器：页面刷新时内部框体刷新，外部滚动条可以往上滑动看前面的内容 -->
          <div class="chat-body" ref="chatBody">
          <!-- 聊天消息区域 -->
          <div class="chat-area" v-if="messages.length > 0">
            <div class="messages-container" ref="messagesContainer">
              <div v-for="(msg, idx) in messages" :key="msg.messageId || `${msg.role}-${msg.created_at}-${idx}`" class="message-wrapper" :class="msg.role"
                   v-show="msg.content || msg.thinkingContent || msg.answerContent || msg.role === 'user'">
                <div class="message-item">
                  <div class="message-avatar" v-if="msg.role === 'assistant'">
                    <img src="/img/ai-avatar.png" alt="AI助手" class="avatar-image" />
                  </div>

                  <div class="message-content">
                    <!-- AI 回答内容 -->
                    <!-- 🔥 分离渲染思考过程和回答内容，避免闪烁 -->
                    <div v-if="msg.role === 'assistant'" class="message-text">
                      <!-- 思考过程：以卡片形式展示完整思考过程 -->
                      <div
                        v-if="msg.thinkingLocked && msg.thinkingContent"
                        v-html="formatThinkingBlock(msg.thinkingContent, msg.messageId)"
                        class="thinking-section"
                      ></div>

                      <!-- 回答内容：使用打字效果逐字显示
                           规则：
                           - 在尚未识别出 </think> 之前（msg.thinkingLocked 为 false），回答区域其实是在流式展示“思考过程”，此时也使用仿宋字体；
                           - 一旦识别到 </think> 并锁定思考块（msg.thinkingLocked 为 true），思考内容移入思考框体，回答区域恢复为默认字体。 -->
                      <div
                        v-if="msg.displayedAnswerContent"
                        v-html="formatAnswerContent(msg.displayedAnswerContent, msg.isStreamFinished)"
                        class="answer-section"
                        :class="{ 'answer-thinking-flow': !msg.thinkingLocked }"
                      ></div>

                      <!-- 兼容旧格式：如果没有分离字段，使用原方法 -->
                      <div v-if="!msg.thinkingLocked && !msg.answerContent && msg.content" v-html="formatMessage(msg.content)"></div>
                    </div>
                    <div class="message-actions" v-if="msg.role === 'assistant'">
                      <div class="message-time">{{ formatTime(msg.created_at) }}</div>
                      <div class="action-buttons">
                        <button class="action-btn" @click="copyMessage(msg)" title="复制">
                          <i class="el-icon-document-copy"></i>
                        </button>
                        <button class="action-btn" @click="rethinkMessage(idx)" :disabled="isLoading" title="重新思考">
                          <i class="el-icon-refresh-left"></i>
                        </button>
                      </div>
                    </div>

                    <!-- 用户消息的内容和文件 -->
                    <div v-if="msg.role === 'user'" class="user-message-content">
                      <div class="message-text user-message-text">
                        <!-- 用户上传的文件 - 使用文件卡片样式 -->
                        <div v-if="msg.uploadedFiles && msg.uploadedFiles.length > 0" class="user-files-cards">
                          <div v-for="(file, fileIdx) in msg.uploadedFiles" :key="fileIdx"
                               class="file-card user-file-card"
                               :data-card-id="`user-file-${fileIdx}`">
                            <div class="file-icon">
                              <i :class="getUserFileIcon(file.name)"></i>
                              <span class="file-ext">.{{ getFileExtension(file.name) }}</span>
                            </div>
                            <div class="file-info">
                              <div class="file-name" :title="file.name">{{ file.name }}</div>
                              <div class="file-size" v-if="file.size">{{ formatFileSize(file.size) }}</div>
                            </div>
                            <a v-if="file.url" :href="file.url" :download="file.name" class="file-download" title="下载文件" @click.stop>
                              <i class="el-icon-download"></i>
                            </a>
                            <div v-else class="file-preview" @click="previewFile(file)" title="预览文件">
                              <i class="el-icon-view"></i>
                            </div>
                          </div>
                        </div>
                        <div class="user-message-text-body" v-html="formatMessage(msg.content)"></div>
                      </div>
                    </div>
                    <div class="message-actions" v-if="msg.role === 'user'">
                      <div class="message-time">{{ formatTime(msg.created_at) }}</div>
                      <div class="action-buttons">
                        <button class="action-btn" @click="copyMessage(msg.content)" title="复制">
                          <i class="el-icon-document-copy"></i>
                        </button>
                        <button class="action-btn" @click="resendMessage(msg.content)" :disabled="isLoading" title="重新发送">
                          <i class="el-icon-refresh"></i>
                        </button>
                      </div>
                    </div>
                  </div>
                  <div class="message-avatar" v-if="msg.role === 'user'">
                    <img src="/img/user-avatar.png" alt="用户" class="avatar-image" />
                  </div>
                </div>
              </div>

              <!-- 加载中提示：仅在还没有收到任何AI回答时显示 -->
              <div v-if="isLoading && !hasAnyAssistantContent" class="message-wrapper assistant">
                <div class="message-item">
                  <div class="message-avatar">
                    <img src="/img/ai-avatar.png" alt="AI助手" class="avatar-image" />
                  </div>
                  <div class="message-content">
                    <div class="typing-indicator">
                      <span></span>
                      <span></span>
                      <span></span>
                    </div>
                  </div>
                </div>
              </div>

              <!-- 下一步问题建议 -->
              <div v-if="!isLoading && currentSuggestedQuestions.length > 0" class="suggested-questions-container">
                <div class="suggested-questions-title">
                  <i class="el-icon-question"></i>
                  <span>你可能还想问</span>
                </div>
                <div class="suggested-questions-list">
                  <div v-for="(question, idx) in currentSuggestedQuestions" :key="idx" class="suggested-question-chip" @click="setQuickMessage(question)">
                    <span>{{ question }}</span>
                    <i class="el-icon-arrow-right"></i>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- 欢迎界面 - 优化后的样式 -->
          <div class="welcome-area" v-else>
            <div class="welcome-chat-container">
              <!-- 欢迎气泡 -->
              <div class="welcome-message-wrapper">
                <div class="welcome-message-item">
                  <!-- 欢迎气泡内容 -->
                  <div class="welcome-bubble">
                    <!-- 上半部分：卡通形象和欢迎语并排 -->
                    <div class="welcome-top-section">
                      <!-- 左侧卡通形象 -->
                      <div class="welcome-avatar-section">
                        <div class="welcome-avatar">
                          <img src="/img/ai-avatar.png" alt="AI智能助手" class="welcome-avatar-image" />

                        </div>
                      </div>
                      <!-- 右侧欢迎语 -->
                      <div class="welcome-greeting-section">
                        <div class="welcome-greeting">
                          {{ appConfig.openingStatement || "嗨~我是Ai智能助手，有什么能够帮助您？" }}
                        </div>
                        <div class="welcome-subtitle" v-if="!appConfig.openingStatement && isAgentMode">
                          我可以帮你回答问题、分析文档、编写代码等
                        </div>
                      </div>
                    </div>

                    <!-- 分隔线 -->
                    <div class="welcome-divider"></div>

                    <!-- 下半部分：提示问题和推荐问题 -->
                    <div class="welcome-bottom-section">
                      <!-- 提示文字 - 左对齐 -->
                      <div class="welcome-tip-section">
                        <i class="el-icon-chat-line-square"></i>
                        <span>您可以这样问一问～</span>
                      </div>

                      <!-- 推荐问题按钮 -->
                      <div class="welcome-suggestions">
                        <template v-if="appConfig.suggestedQuestions.length > 0">
                          <button
                            v-for="(question, idx) in appConfig.suggestedQuestions"
                            :key="idx"
                            class="welcome-suggestion-btn"
                            @click="setQuickMessage(question)">
                            <!-- 第1个问题：Q1图标 -->
                            <img v-if="idx === 0" class="suggestion-icon" src="/img/Q1.svg" alt="Q1" />
                            <!-- 第2个问题：Q2图标 -->
                            <img v-else-if="idx === 1" class="suggestion-icon" src="/img/Q2.svg" alt="Q2" />
                            <!-- 第3个问题：Q3图标 -->
                            <img v-else-if="idx === 2" class="suggestion-icon" src="/img/Q3.svg" alt="Q3" />
                            <!-- 第4个及以后问题：Element UI图标 -->
                            <i v-else class="el-icon-chat-dot-square"></i>
                            <span>{{ question }}</span>
                          </button>
                        </template>
                        <template v-else-if="isAgentMode">
                          <button class="welcome-suggestion-btn" @click="setQuickMessage('帮我分析一下这个Excel文档')">
                            <img class="suggestion-icon" src="/img/Q1.svg" alt="Q1" />
                            <span>分析文档</span>
                          </button>
                          <button class="welcome-suggestion-btn" @click="setQuickMessage('帮我写一段代码')">
                            <img class="suggestion-icon" src="/img/Q2.svg" alt="Q2" />
                            <span>编写代码</span>
                          </button>
                          <button class="welcome-suggestion-btn" @click="setQuickMessage('今天的日期是什么')">
                            <img class="suggestion-icon" src="/img/Q3.svg" alt="Q3" />
                            <span>查询信息</span>
                          </button>
                        </template>
                      </div>

                      <!-- 变量输入提示 -->
                      <div class="welcome-variable-tip" v-if="shouldShowVariablesPanel && requiredVariables.length > 0">
                        <i class="el-icon-warning"></i>
                        <span>开始使用前，请先填写必填信息：{{ requiredVariables.map(v => v.label || v.variable).join('、') }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          </div>
          <!-- 关闭 chat-body -->


          <!-- 变量输入面板（隐藏独立面板，改为合并到输入框） -->
          <div class="variables-panel" v-if="false">
            <div class="panel-header" @click="variablesPanelCollapsed = !variablesPanelCollapsed">
              <div class="header-left">
                <i class="el-icon-setting"></i>
                <span class="panel-title">输入参数</span>
                <span class="variable-count">{{ appConfig.variables.length }} 个变量</span>
              </div>
              <i :class="variablesPanelCollapsed ? 'el-icon-arrow-down' : 'el-icon-arrow-up'"></i>
            </div>

            <div class="panel-content" v-show="!variablesPanelCollapsed">
              <div v-for="variable in appConfig.variables" :key="variable.variable || variable.name" class="variable-item">
                <div class="variable-label">
                  <span>{{ variable.label || variable.variable }}</span>
                  <span v-if="variable.required" class="required-mark">必填</span>
                  <span v-else class="optional-mark">可选</span>
                </div>

                <!-- 文本输入 -->
                <el-input v-if="variable.type === 'text-input' || variable.type === 'text'" v-model="variableValues[variable.variable || variable.name]" :placeholder="'请输入' + (variable.label || variable.variable)"
                          :maxlength="variable.max_length || 48" clearable />

                <!-- 段落输入 -->
                <el-input v-else-if="variable.type === 'paragraph'" type="textarea" v-model="variableValues[variable.variable || variable.name]" :placeholder="'请输入' + (variable.label || variable.variable)" :rows="3"
                          :maxlength="variable.max_length || 1000" clearable />

                <!-- 数字输入 -->
                <el-input-number v-else-if="variable.type === 'number'" v-model="variableValues[variable.variable || variable.name]" :min="variable.min || 0" :max="variable.max || 999999" controls-position="right" style="width: 100%" />

                <!-- 选择器 -->
                <el-select v-else-if="variable.type === 'select'" v-model="variableValues[variable.variable || variable.name]" :placeholder="'请选择' + (variable.label || variable.variable)" style="width: 100%" clearable>
                  <el-option v-for="option in variable.options" :key="option" :label="option" :value="option" />
                </el-select>

                <!-- 单文件上传 -->
                <div v-else-if="variable.type === 'file'" class="file-upload-area">
                  <input :ref="'file_' + (variable.variable || variable.name)" type="file" class="hidden-file" @change="handleVariableFileSelect(variable, $event)" />

                  <div v-if="!variableValues[variable.variable || variable.name]" class="upload-placeholder">
                    <el-button icon="el-icon-upload" size="small" @click="$refs['file_' + (variable.variable || variable.name)][0].click()">
                      选择文件
                    </el-button>
                    <span class="upload-hint">
                      仅支持常见文档类型（如 txt、md、pdf、html、xlsx、xls、docx、csv 等），其他类型将无法被文档提取
                    </span>
                  </div>

                  <div v-else class="file-card">
                    <i class="el-icon-document"></i>
                    <div class="file-info">
                      <div class="file-name">{{ variableValues[variable.variable || variable.name].name }}</div>
                      <div class="file-size">{{ formatSize(variableValues[variable.variable || variable.name].size) }}</div>
                    </div>
                    <el-button type="danger" icon="el-icon-delete" size="mini" circle @click="removeVariableFile(variable)">
                    </el-button>
                  </div>
                </div>

                <!-- 文件列表上传 -->
                <div v-else-if="variable.type === 'file-list'" class="file-list-upload-area">
                  <input :ref="'filelist_' + (variable.variable || variable.name)" type="file" multiple class="hidden-file" @change="handleVariableFileListSelect(variable, $event)" />

                  <el-button icon="el-icon-upload" size="small" @click="$refs['filelist_' + (variable.variable || variable.name)][0].click()">
                    添加文件
                  </el-button>
                  <span class="upload-hint">
                    仅支持常见文档类型（如 txt、md、pdf、html、xlsx、xls、docx、csv 等），其他类型将无法被文档提取
                  </span>

                  <div v-if="variableValues[variable.variable || variable.name] && variableValues[variable.variable || variable.name].length > 0" class="file-list">
                    <div v-for="(file, idx) in variableValues[variable.variable || variable.name]" :key="file.uid" class="file-card">
                      <i class="el-icon-document"></i>
                      <div class="file-info">
                        <div class="file-name">{{ file.name }}</div>
                        <div class="file-size">{{ formatSize(file.size) }}</div>
                      </div>
                      <el-button type="danger" icon="el-icon-delete" size="mini" circle @click="removeVariableFileFromList(variable, idx)">
                      </el-button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- 输入区域 -->
          <div class="input-area">
            <div class="input-wrapper">
              <div class="composer" @keyup.enter="handleSend()">
                <!-- 附件列表 - 有附件时显示 -->
                <div class="attachments" v-if="attachments.length">
                  <div class="attachments-header">
                    <i class="el-icon-folder-opened"></i>
                    <span class="attachments-count">已选择 {{ attachments.length }} 个文件</span>
                  </div>
                  <div class="attachment-list">
                    <div v-for="(file, idx) in attachments" :key="file.uid || idx" class="attachment-card">
                      <button class="att-remove" type="button" @click.stop="removeAttachment(idx)" title="移除">
                        <i class="el-icon-close"></i>
                      </button>
                      <div class="file-icon">
                        <i class="el-icon-document"></i>
                      </div>
                      <div class="file-meta">
                        <div class="file-name">{{ file.name }}</div>
                        <div class="file-size">{{ formatSize(file.size) }}</div>
                      </div>
                    </div>
                  </div>
                </div>

                <div class="input-row">
                  <textarea v-model="message" ref="textarea" class="input" rows="1" placeholder="给 AI 发送消息 (Enter 发送, Shift+Enter 换行)" @keydown="handleKeydown" :disabled="isLoading"></textarea>
                  <div class="actions">
                    <!-- 变量输入参数按钮（弹出面板） -->
                    <el-popover v-if="shouldShowVariablesPanel" ref="variablesPopover" placement="top" width="420" trigger="manual" v-model="variablesPopoverVisible" popper-class="composer-variables-popover">
                      <!-- 变量面板内容 -->
                      <div class="composer-variables">
                        <div v-if="appConfig.variables.length === 0" class="empty-state">
                          <p>暂无变量配置</p>
                        </div>
                        <div v-else class="variables-form">
                          <div v-for="variable in appConfig.variables" :key="variable.variable || variable.name" class="variable-item">
                            <!-- 变量标签和输入区域 -->
                            <div class="variable-row">
                              <label class="variable-label">
                                {{ variable.label || variable.variable || variable.name }}
                                <span v-if="variable.required" class="required-mark">*</span>
                              </label>
                              <div class="variable-input-area">

                            <!-- 文本输入 -->
                            <template v-if="variable.type === 'text' || variable.type === 'paragraph' || variable.type === 'string'">
                              <textarea v-if="variable.type === 'paragraph'"
                                v-model="variableValues[variable.variable || variable.name]"
                                class="variable-input textarea"
                                :placeholder="variable.placeholder || ''"
                                rows="3">
                              </textarea>
                              <input v-else
                                v-model="variableValues[variable.variable || variable.name]"
                                type="text"
                                class="variable-input"
                                :placeholder="variable.placeholder || ''">
                            </template>

                            <!-- 数字输入 -->
                            <template v-else-if="variable.type === 'number'">
                              <input v-model.number="variableValues[variable.variable || variable.name]"
                                type="number"
                                class="variable-input"
                                :placeholder="variable.placeholder || ''">
                            </template>

                            <!-- 下拉选择 -->
                            <template v-else-if="variable.type === 'select'">
                              <select v-model="variableValues[variable.variable || variable.name]" class="variable-input">
                                <option value="">-- 请选择 --</option>
                                <option v-for="option in variable.options" :key="option" :value="option">
                                  {{ option }}
                                </option>
                              </select>
                            </template>

                            <!-- 文件上传 -->
                            <template v-else-if="variable.type === 'file'">
                              <div class="file-upload-wrapper">
                                <input :ref="`fileInput_${variable.variable || variable.name}`"
                                  type="file"
                                  class="hidden-file"
                                  @change="(e) => handleVariableFileUpload(e, variable.variable || variable.name, false)">

                                <!-- 未上传文件时显示选择按钮 -->
                                <div v-if="!variableValues[variable.variable || variable.name]" class="file-control-row">
                                  <button type="button"
                                    class="file-upload-trigger"
                                    @click="() => $refs[`fileInput_${variable.variable || variable.name}`][0].click()">
                                    <i class="el-icon-upload"></i> 选择文件
                                  </button>
                                </div>

                                <!-- 已上传文件时显示文件名和删除按钮 -->
                                <div v-else class="file-display-row">
                                  <div class="file-name clickable"
                                    @click="() => $refs[`fileInput_${variable.variable || variable.name}`][0].click()"
                                    title="点击更换文件">
                                    <i class="el-icon-document"></i>
                                    {{ variableValues[variable.variable || variable.name].name }}
                                  </div>
                                  <button type="button"
                                    class="file-remove-btn"
                                    @click="variableValues[variable.variable || variable.name] = null"
                                    title="删除文件">
                                    <i class="el-icon-delete"></i>
                                  </button>
                                </div>
                              </div>
                            </template>

                            <!-- 文件列表上传 -->
                            <template v-else-if="variable.type === 'file-list'">
                              <div class="file-list-wrapper">
                                <input :ref="`fileListInput_${variable.variable || variable.name}`"
                                  type="file"
                                  multiple
                                  class="hidden-file"
                                  @change="(e) => handleVariableFileUpload(e, variable.variable || variable.name, true)">
                                <div class="file-control-row">
                                  <button type="button"
                                    class="file-upload-trigger"
                                    @click="() => $refs[`fileListInput_${variable.variable || variable.name}`][0].click()">
                                    <i class="el-icon-upload"></i> 选择文件
                                  </button>
                                </div>
                                <div v-if="variableValues[variable.variable || variable.name] && variableValues[variable.variable || variable.name].length > 0" class="file-list">
                                  <div v-for="(file, idx) in variableValues[variable.variable || variable.name]" :key="idx" class="file-item">
                                    <div class="file-info">
                                      <i class="el-icon-document"></i>
                                      <span>{{ file.name }}</span>
                                    </div>
                                    <button type="button" class="file-remove-btn" @click="() => removeVariableFile(variable.variable || variable.name, idx)" title="删除文件">
                                      <i class="el-icon-delete"></i>
                                    </button>
                                  </div>
                                </div>
                              </div>
                            </template>

                            <!-- 未知类型的通用处理（后备方案） -->
                            <template v-else>
                              <input v-model="variableValues[variable.variable || variable.name]"
                                type="text"
                                class="variable-input"
                                :placeholder="variable.placeholder || '请输入变量'">
                            </template>

                              </div>
                            </div>
                          </div>
                        </div>
                      </div>
                      <div slot="reference" class="popover-center-ref"></div>
                    </el-popover>

                    <button v-if="shouldShowVariablesPanel" class="action-button parameters-btn" :class="{
                'alert': hasMissingRequired,
                'active': variablesPopoverVisible
              }" type="button" :disabled="isLoading" @click="variablesPopoverVisible = !variablesPopoverVisible" title="设置参数">
                      <i class="el-icon-setting"></i>
                      <sup v-if="missingRequiredCount > 0" class="badge">{{ missingRequiredCount }}</sup>
                    </button>

                    <!-- Chatflow 文件上传按钮 -->
                    <template v-if="shouldShowFileUpload">
                      <input ref="chatflowFile" class="hidden-file" type="file" multiple accept="*" @change="handleChatflowFileUpload" />
                      <button class="action-button file-upload-btn" type="button" @click="triggerChatflowFileUpload" title="上传文件" :disabled="isLoading">
                        <i class="el-icon-paperclip"></i>
                      </button>
                    </template>

                    <button
                      v-if="isLoading"
                      class="action-button stop-button"
                      type="button"
                      @click="stopGeneration"
                      :title="isCurrentStreamServerDone ? '全部输出' : '停止生成'"
                    >
                      <i :class="isCurrentStreamServerDone ? 'el-icon-finished' : 'el-icon-video-pause'"></i>
                    </button>
                    <button v-else class="action-button send-button" type="button" @click="handleSend()" title="发送" :disabled="!message.trim()">
                      <i class="el-icon-s-promotion"></i>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>

        </div>
      </div>
    </div>
  </div>
</template>

<script>
import NavBar from "@/layout/UserView/NavBar.vue";
import FontLoader from "@/components/UserView/general/font.vue";
import ChatSidebar from "@/components/UserView/general/sidebar.vue";
import {
  sendChatMessage,
  uploadFile,
  getConversations,
  deleteConversation,
  stopChatMessage,
  getMessages,
  getAppParameters,
} from "@/api/dify/chat";
import { getCurrentUser } from "@/utils/userService";

// 调试开关：用于查看 Dify 流式返回的原始数据（思考内容 / 表格 / HTML 等）
// 默认关闭，如需排查问题可临时改为 true
const DEBUG_STREAM_LOG = false;

// Dify 文档提取器 / sys.files 支持的文档扩展名（用于前端校验与提示）
// 需要与 Dify 后台「文档提取器」中的配置保持一致
const DIFY_SUPPORTED_DOC_EXTS = [
  "txt",
  "md",
  "mdx",
  "markdown",
  "pdf",
  "html",
  "htm",
  "xlsx",
  "xls",
  "docx",
  "csv",
  "vtt",
  "properties",
  "xml",
  "epub",
  "ppt",
  "pptx",
  "eml",
  "msg",
];

const DIFY_SUPPORTED_DOC_EXTS_TEXT = DIFY_SUPPORTED_DOC_EXTS.map(
  (ext) => `.${ext}`
).join("、");

// 全局函数：切换思考过程显示/隐藏
window.toggleThinkBlock = function (thinkId) {
  const content = document.getElementById(`content-${thinkId}`);
  const icon = document.getElementById(`icon-${thinkId}`);

  if (content && icon) {
    if (content.classList.contains("collapsed")) {
      // 展开
      content.classList.remove("collapsed");
      icon.classList.remove("el-icon-arrow-down");
      icon.classList.add("el-icon-arrow-up");
    } else {
      // 折叠
      content.classList.add("collapsed");
      icon.classList.remove("el-icon-arrow-up");
      icon.classList.add("el-icon-arrow-down");
    }
  }
};

export default {
  name: "AIChat",
  components: {
    NavBar,
    FontLoader,
    ChatSidebar,
  },
  data() {
    return {
      message: "",
      messages: [],
      attachments: [],
      sidebarCollapsed: false,
      showHistoryPanel: true,
      currentConversationId: null,
      conversations: [],
      isLoading: false,
      isLoadingHistory: false, // 新增：历史记录加载状态
      userId: "", // 用户标识（loginName或userSn），用于传给后端
      userName: "", // 用户名（userName），用于页面显示
      currentTaskId: null,
      abortController: null,
      streamController: null,
      // Dify 应用配置
      appConfig: {
        openingStatement: "",
        suggestedQuestions: [],
        suggestedQuestionsAfterAnswer: false,
        fileUploadEnabled: false, // Chatflow 文件上传功能是否启用（对应 sys.files）
        userInputForm: [], // 用户输入表单配置（包含所有变量定义）
        variables: [], // 所有类型的变量列表（从 userInputForm 中提取）
        mode: "", // 应用模式：agent, chat, completion, workflow 等
      },
      // 当前消息的下一步建议
      currentSuggestedQuestions: [],
      // 变量输入面板
      variablesPanelCollapsed: false, // 变量面板是否折叠
      variableValues: {}, // 当前输入的变量值 { variableName: value }
      // 当前会话已提交的变量（用于续接会话）
      conversationInputs: {}, // 存储已提交的变量，用于续接会话时重复传递
      // 变量弹出面板控制
      variablesPopoverVisible: false,
      // App名称
      appName: null,
      // 页面禁用标记（缺少必要参数时使用）
      pageDisabled: false,
      // 🔥 用户手动滚动标记：当用户向上滚动时设为true，阻止自动滚动到底部
      userHasScrolledUp: false,
      // 🔥 上一次滚动位置，用于检测滚动方向
      lastScrollTop: 0,
    };
  },
  computed: {
    // 是否显示文件上传按钮（仅判断 Chatflow 文件上传功能）
    shouldShowFileUpload() {
      return this.appConfig.fileUploadEnabled;
    },

    // 是否显示变量输入面板
    shouldShowVariablesPanel() {
      return this.appConfig.variables.length > 0;
    },

    // 获取必填的变量
    requiredVariables() {
      return this.appConfig.variables.filter((v) => v.required);
    },

    // 是否为 Agent 模式
    isAgentMode() {
      return this.appConfig.mode === "agent";
    },

    // 🔥 检查最后一条助手消息是否已有内容（用于控制加载动画显示）
    hasAnyAssistantContent() {
      // 从后往前找第一条助手消息
      for (let i = this.messages.length - 1; i >= 0; i--) {
        const msg = this.messages[i];
        if (msg.role === "assistant") {
          // 检查是否有任何内容
          return !!(msg.content || msg.thinkingContent || msg.answerContent);
        }
      }
      return false;
    },

    // 未填必填项数量与状态（用于显著提示）
    missingRequiredCount() {
      if (!this.appConfig || !this.appConfig.variables) return 0;
      const required = this.appConfig.variables.filter((v) => v.required);
      let count = 0;
      required.forEach((v) => {
        const key = v.variable || v.name;
        const val = this.variableValues[key];
        if (v.type === "file" || v.type === "file-list") {
          if (!val || (Array.isArray(val) && val.length === 0)) count++;
        } else if (!val || val.toString().trim() === "") {
          count++;
        }
      });
      return count;
    },

    hasMissingRequired() {
      return this.missingRequiredCount > 0 && !this.currentConversationId;
    },

    // 主内容区动态居中样式（将消息区和输入框作为整体进行居中）
    mainContentStyle() {
      // 侧边栏宽度为 300px
      const sidebarWidth = 300;
      // 当侧边栏打开时，向右偏移侧边栏宽度的一半，使整个内容区在剩余空间居中
      const offset = this.showHistoryPanel ? sidebarWidth / 2 : 0;
      return {
        marginLeft: `${offset}px`,
        transition: "margin-left 0.3s ease",
      };
    },

    // 当前这一条正在显示中的助手消息，服务端流是否已经结束
    isCurrentStreamServerDone() {
      // 从后往前找第一条尚未完成渲染的助手消息
      for (let i = this.messages.length - 1; i >= 0; i--) {
        const msg = this.messages[i];
        if (msg.role === "assistant" && !msg.isStreamFinished) {
          return !!msg.serverStreamDone;
        }
      }
      return false;
    },
  },
  async mounted() {
    // 从路由参数获取 App名称
    if (this.$route.query.appName) {
      this.appName = this.$route.query.appName;
      if (DEBUG_STREAM_LOG) {
        console.log("📝 从路由参数获取到 App名称:", this.appName);
      }
    }
    // 缺少必要参数则禁用整页并提示
    if (!this.appName || String(this.appName).trim() === "") {
      this.pageDisabled = true;
      this.$message.error("缺少必要参数：appName");
      return;
    }

    // 首先检查并获取用户标识
    await this.initializeUserId();

    await this.loadAppConfig();
    await this.loadConversationsList();
    this.autoResizeTextarea();

    // 如果有会话历史，可以选择自动加载最近的一个对话（可选）
    // if (this.conversations.length > 0) {
    //   await this.loadConversation(this.conversations[0].id)
    // }

    // 添加全局点击监听，用于点击外部关闭弹窗
    document.addEventListener("click", this.handleClickOutside);

    // 🔥 添加 chat-body 滚动和滚轮监听，检测用户是否手动滚动
    this.$nextTick(() => {
      if (this.$refs.chatBody) {
        this.$refs.chatBody.addEventListener("scroll", this.handleChatBodyScroll);
        // 🔥 监听滚轮事件，在滚动发生前检测用户意图
        this.$refs.chatBody.addEventListener("wheel", this.handleChatBodyWheel, { passive: true });
      }
    });
  },
  beforeDestroy() {
    // 组件销毁时清理资源
    if (this.abortController) {
      this.abortController.abort();
    }
    if (this.streamController && this.streamController.abort) {
      this.streamController.abort();
    }

    // 移除全局点击监听
    document.removeEventListener("click", this.handleClickOutside);

    // 🔥 移除 chat-body 滚动和滚轮监听
    if (this.$refs.chatBody) {
      this.$refs.chatBody.removeEventListener("scroll", this.handleChatBodyScroll);
      this.$refs.chatBody.removeEventListener("wheel", this.handleChatBodyWheel);
    }
  },
  methods: {
    /**
     * 格式化文件大小
     */
    formatFileSize(bytes) {
      if (!bytes || bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
    },

    /**
     * 根据文件扩展名获取图标
     */
    getFileIcon(ext) {
      const iconMap = {
        // 文档
        'doc': 'document', 'docx': 'document', 'pdf': 'document',
        'txt': 'document', 'rtf': 'document',
        // 表格
        'xls': 'tickets', 'xlsx': 'tickets', 'csv': 'tickets',
        // 演示
        'ppt': 'camera', 'pptx': 'camera',
        // 压缩包
        'zip': 'folder-opened', 'rar': 'folder-opened', '7z': 'folder-opened',
        // 图片
        'jpg': 'picture', 'jpeg': 'picture', 'png': 'picture', 'gif': 'picture',
        // 代码
        'js': 'document', 'ts': 'document', 'json': 'document', 'html': 'document',
      };
      return iconMap[ext] || 'document';
    },

    /**
     * 获取用户上传文件的图标（返回完整的 Element UI 图标类名）
     */
    getUserFileIcon(filename) {
      if (!filename) return 'el-icon-document';
      const ext = this.getFileExtension(filename);
      const icon = this.getFileIcon(ext);
      return `el-icon-${icon}`;
    },

    /**
     * 获取文件扩展名
     */
    getFileExtension(filename) {
      if (!filename) return '';
      const parts = filename.split('.');
      return parts.length > 1 ? parts.pop().toLowerCase() : '';
    },

    /**
     * 判断文件名是否为 Dify 文档提取器支持的类型
     */
    isDifySupportedDocFile(filename) {
      const ext = this.getFileExtension(filename);
      return !!ext && DIFY_SUPPORTED_DOC_EXTS.includes(ext);
    },
    
    /**
     * 智能检测 URL 是否是内部地址（需要通过后端代理）
     * @param {string} url - 要检测的 URL
     * @returns {boolean} - 是否需要代理
     *
     * 支持环境变量配置：
     * - VUE_APP_FORCE_FILE_PROXY: 强制使用代理（true/false）
     * - VUE_APP_INTERNAL_URL_PREFIXES: 内部 URL 前缀白名单（逗号分隔）
     */
    isInternalUrl(url) {
      if (!url) return false;

      // 检查是否强制使用代理（环境变量配置）
      const forceProxy = process.env.VUE_APP_FORCE_FILE_PROXY === 'true';
      if (forceProxy) {
        console.log('🔧 [配置] 强制使用文件代理');
        return true;
      }

      // 检查是否在白名单中（环境变量配置）
      const internalPrefixes = process.env.VUE_APP_INTERNAL_URL_PREFIXES;
      if (internalPrefixes) {
        const prefixes = internalPrefixes.split(',').map(p => p.trim()).filter(p => p);
        for (const prefix of prefixes) {
          if (url.startsWith(prefix)) {
            console.log(`🔧 [配置] URL 匹配白名单前缀: ${prefix}`);
            return true;
          }
        }
        // 如果配置了白名单但不匹配，则不使用代理
        return false;
      }

      // 没有配置环境变量，使用智能检测
      try {
        const urlObj = new URL(url);
        const hostname = urlObj.hostname.toLowerCase();

        // 1. 检测 localhost 和回环地址
        if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
          return true;
        }

        // 2. 检测容器内部地址（如 api, dify, redis 等单词主机名）
        // 容器名称通常不包含点号
        if (!hostname.includes('.') && hostname !== 'localhost') {
          // 这是容器主机名（如 api:5001, dify:80）
          return true;
        }

        // 3. 检测私有/内网 IP 地址段
        // 标准私有 IP:
        // - 10.0.0.0 - 10.255.255.255 (A类私有网络)
        // - 172.16.0.0 - 172.31.255.255 (B类私有网络)
        // - 192.168.0.0 - 192.168.255.255 (C类私有网络)
        //
        // 扩展内网 IP (常见于机构内部):
        // - 11.x.x.x (部分运营商/机构内网)
        // - 100.64.0.0 - 100.127.255.255 (运营商级NAT)
        const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
        const match = hostname.match(ipv4Regex);

        if (match) {
          const [, a, b, c, d] = match.map(Number);

          // 检查是否是有效的 IP 地址
          if (a <= 255 && b <= 255 && c <= 255 && d <= 255) {
            // 1. 标准私有 IP 段
            if (a === 10) return true; // 10.0.0.0/8
            if (a === 11) return true; // 11.0.0.0/8 (常见于机构内网，如你的配置)
            if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
            if (a === 192 && b === 168) return true; // 192.168.0.0/16

            // 2. 运营商级NAT (RFC 6598)
            if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10

            // 3. 回环地址
            if (a === 127) return true; // 127.0.0.0/8
          }
        }

        // 4. 其他情况认为是公网地址
        return false;
      } catch (e) {
        // URL 解析失败，可能是相对路径或格式错误
        console.warn('⚠️ URL 解析失败:', url, e);
        return false;
      }
    },

    /**
     * 处理点击外部关闭弹窗
     */
    handleClickOutside(event) {
      // 处理变量弹窗
      if (this.variablesPopoverVisible) {
        const popoverEl = this.$refs.variablesPopover?.$el;
        const popperEl = document.querySelector(".composer-variables-popover");
        const targetBtn = event.target.closest(".parameters-btn");
        if (
          !targetBtn &&
          !popoverEl?.contains(event.target) &&
          !popperEl?.contains(event.target)
        ) {
          this.variablesPopoverVisible = false;
        }
      }

      // 历史抽屉只能通过关闭按钮关闭，不响应外部点击
    },

    /**
     * 初始化用户标识
     * 从后端接口获取当前登录用户信息
     */
    async initializeUserId() {
      try {
        // 从后端获取当前用户信息
        const userInfo = await getCurrentUser();

        if (userInfo && userInfo.loginName) {
          // 使用 loginName 作为用户标识（传给后端）
          this.userId = userInfo.loginName;
          // 使用 userName 作为显示名称
          this.userName = userInfo.userName || userInfo.loginName;
          console.log("✅ 从后端获取用户信息成功:", {
            userId: this.userId,
            userName: this.userName,
          });
        } else {
          // 如果获取失败，使用 userSn 或生成默认值
          this.userId = userInfo?.userSn || "user_" + Date.now();
          this.userName = userInfo?.userName || "用户";
          console.warn("⚠️ 获取用户信息不完整，使用备用值");
        }
      } catch (error) {
        console.error("❌ 获取用户信息失败:", error);
        // 如果后端接口失败，生成一个默认的用户标识
        this.userId = "user_" + Date.now();
        this.userName = "用户";
        this.$message.warning("获取用户信息失败，使用临时用户标识");
      }
    },

    async loadAppConfig() {
      try {
        console.log("🔄 正在加载应用配置... App名称:", this.appName);
        const config = await getAppParameters(this.appName);

        // 打印完整的 API 返回数据，方便调试
        console.log("📥 API 返回的完整配置:", JSON.stringify(config, null, 2));

        if (config) {
          // 优先从 data 字段读取，兼容不同的返回格式
          const configData = config.data || config;

          this.appConfig.openingStatement = configData.opening_statement || "";
          this.appConfig.suggestedQuestions =
            configData.suggested_questions || [];
          this.appConfig.suggestedQuestionsAfterAnswer =
            configData.suggested_questions_after_answer?.enabled || false;

          // 读取应用模式
          this.appConfig.mode = (
            config.mode ||
            configData.mode ||
            config.app_mode ||
            ""
          ).toLowerCase();

          // 打印推荐问题配置
          console.log("🎯 推荐问题配置:", {
            欢迎界面推荐问题:
              this.appConfig.suggestedQuestions.length > 0
                ? this.appConfig.suggestedQuestions
                : "未配置",
            消息后推荐问题: this.appConfig.suggestedQuestionsAfterAnswer
              ? "已启用"
              : "未启用",
            原始配置: config.suggested_questions_after_answer,
          });

          // 1. 检查 Chatflow 文件上传功能是否启用（对应 sys.files）
          // 这个功能用于多模态对话，文件通过 files 参数传递
          const fileUploadEnabled =
            configData.file_upload?.enabled ||
            config.features?.file_upload?.enabled ||
            false;
          this.$set(this.appConfig, 'fileUploadEnabled', fileUploadEnabled);

          console.log("🔍 检查文件上传配置:", {
            "configData.file_upload": configData.file_upload,
            "config.features": config.features,
            fileUploadEnabled: this.appConfig.fileUploadEnabled,
            enabled: configData.file_upload?.enabled,
            "⚠️ 按钮应该显示吗": this.appConfig.fileUploadEnabled ? "是" : "否",
          });

          // 2. 获取用户输入表单配置（可能包含文件变量定义）
          // 文件变量用于工作流输入，文件通过 inputs 参数传递
          this.appConfig.userInputForm = configData.user_input_form || [];

          console.log("🔍 用户输入表单配置:", {
            "config.user_input_form": config.user_input_form,
            userInputFormLength: this.appConfig.userInputForm.length,
            userInputFormDetail: this.appConfig.userInputForm,
          });

          // 3. 提取所有类型的变量列表
          // 注意：user_input_form 的结构是 [{变量名: {配置}}]
          this.appConfig.variables = [];
          this.appConfig.userInputForm.forEach((inputWrapper) => {
            // inputWrapper 是类似 {"variable_name": {...}} 的对象
            Object.keys(inputWrapper).forEach((varName) => {
              const inputConfig = inputWrapper[varName];
              // 添加所有类型的变量
              // 确保 variable 字段存在且与 key 一致
              this.appConfig.variables.push({
                ...inputConfig,
                variable: inputConfig.variable || varName, // 使用配置中的 variable 字段或使用 key
                name: varName, // 保留原始 key
              });
            });
          });

          // 初始化变量默认值
          const initialValues = {};
          this.appConfig.variables.forEach((v) => {
            const varName = v.variable || v.name;
            // 根据类型设置默认值
            if (v.type === "file" || v.type === "file-list") {
              initialValues[varName] = v.type === "file-list" ? [] : null;
            } else if (v.type === "select") {
              initialValues[varName] =
                v.default || (v.options && v.options[0]) || "";
            } else {
              initialValues[varName] = v.default || "";
            }
          });
          // 一次性设置所有属性，确保响应式
          this.variableValues = initialValues;

          console.log("📋 变量默认值初始化:", initialValues);
          console.log(
            "📋 变量详细信息:",
            this.appConfig.variables.map((v) => ({
              variable: v.variable,
              name: v.name,
              label: v.label,
              type: v.type,
              required: v.required,
              defaultValue: initialValues[v.variable || v.name],
            }))
          );

          console.log("📋 应用配置加载完成:", {
            mode: this.appConfig.mode,
            isAgentMode: this.isAgentMode,
            fileUploadEnabled: this.appConfig.fileUploadEnabled,
            variablesCount: this.appConfig.variables.length,
            variables: this.appConfig.variables.map((v) => ({
              variable: v.variable,
              name: v.name,
              label: v.label,
              type: v.type,
              required: v.required,
            })),
            shouldShowVariablesPanel: this.shouldShowVariablesPanel,
            shouldShowFileUpload: this.shouldShowFileUpload,
          });

          // 强制更新视图
          this.$nextTick(() => {
            this.$forceUpdate();
            console.log("🔄 强制更新视图完成，shouldShowFileUpload:", this.shouldShowFileUpload);
          });

          // 显示配置说明
          console.log("ℹ️ 应用模式:", this.appConfig.mode || "未指定");
          if (this.appConfig.variables.length > 0) {
            const varSummary = this.appConfig.variables
              .map(
                (v) =>
                  `${v.label || v.variable}[${v.type}]${
                    v.required ? "(必填)" : "(可选)"
                  }`
              )
              .join("、");
            console.log("ℹ️ 变量配置:", varSummary);
          }
          if (this.appConfig.fileUploadEnabled) {
            console.log("ℹ️ 文件上传配置: 启用 Chatflow 文件上传（sys.files）");
          }
          if (this.isAgentMode) {
            console.log("ℹ️ Agent 模式: 将显示快捷操作卡片（如无推荐问题）");
          }
          if (this.appConfig.suggestedQuestions.length > 0) {
            console.log(
              "ℹ️ 欢迎界面推荐问题:",
              this.appConfig.suggestedQuestions.length,
              "个"
            );
          }
          if (this.appConfig.suggestedQuestionsAfterAnswer) {
            console.log("ℹ️ 消息后推荐问题: 已启用，AI 回复后可能显示推荐问题");
          }
        } else {
          console.warn("⚠️ API 未返回配置数据");
        }
      } catch (error) {
        console.error("❌ 加载应用配置失败:", error);
        console.error("错误详情:", {
          message: error.message,
          response: error.response?.data,
          status: error.response?.status,
        });
      }
    },

    /**
     * 发送消息 / 重新思考
     * @param {Object} options
     *  - fromRethink: 是否来自“重新思考”（不再追加新的用户气泡）
     *  - reuseContent: 重新思考时使用的用户问题内容
     */
    async handleSend(options = {}) {
      if (this.isLoading) return;

      const fromRethink = Boolean(options.fromRethink);
      const reuseContent =
        typeof options.reuseContent === "string" ? options.reuseContent : "";

      // 1) 取得要发送的用户问题文案
      let userMessage = "";
      if (fromRethink) {
        userMessage = reuseContent.trim();
      } else {
        if (!this.message.trim()) return;
        userMessage = this.message.trim();
        this.message = "";
      }

      // 新会话时，校验必填变量
      if (!this.currentConversationId && this.appConfig.variables.length > 0) {
        const missingRequired = this.requiredVariables.filter((v) => {
          const varName = v.variable || v.name;
          const value = this.variableValues[varName];
          // 检查值是否为空
          if (v.type === "file" || v.type === "file-list") {
            return !value || (Array.isArray(value) && value.length === 0);
          }
          return !value || value.toString().trim() === "";
        });

        if (missingRequired.length > 0) {
          const varNames = missingRequired
            .map((v) => v.label || v.variable)
            .join("、");
          this.$message.error(`请先填写必填变量：${varNames}`);
          // 打开变量弹出面板
          this.variablesPopoverVisible = true;
          return;
        }
      }

      const currentTime = Date.now();

      // 清空当前的建议问题（发送新消息时）
      this.currentSuggestedQuestions = [];

      // 保存附件信息到临时变量（用于上传）
      // 重新思考时不再重复上传附件，只复用原有用户问题文本
      const attachmentsToUpload = fromRethink ? [] : [...this.attachments];

      // 保存上传的文件信息（用于显示和预览）
      const uploadedFilesInfo =
        !fromRethink && attachmentsToUpload.length > 0
          ? attachmentsToUpload.map((f) => ({
              name: f.name,
              size: f.size,
              raw: f.raw,
              url: f.raw ? URL.createObjectURL(f.raw) : null,
            }))
          : [];

      // 🔥 添加文件类型变量的文件信息到 uploadedFilesInfo
      if (this.appConfig.variables && this.appConfig.variables.length > 0) {
        for (const variable of this.appConfig.variables) {
          const varName = variable.variable || variable.name;
          const value = this.variableValues[varName];

          // 处理文件类型变量
          if ((variable.type === "file" || variable.type === "file-list") && value) {
            const filesToAdd = Array.isArray(value) ? value : [value];
            filesToAdd.forEach(file => {
              if (file && file.name) {
                uploadedFilesInfo.push({
                  name: file.name,
                  size: file.size || 0,
                  raw: file.raw,
                  url: file.raw ? URL.createObjectURL(file.raw) : null,
                });
              }
            });
          }
        }
      }

      // 立即清空附件列表（隐藏文件列表）
      if (!fromRethink) {
        this.attachments = [];
      }

      // 只有“正常发送”时才追加新的用户气泡；
      // 重新思考场景只保留原来的用户气泡，避免列表中出现重复问题
      if (!fromRethink) {
        this.messages.push({
          role: "user",
          content: userMessage,
          created_at: currentTime,
          messageId: `temp-user-${currentTime}`,
          uploadedFiles: uploadedFilesInfo, // 保存文件信息用于显示（包括附件和变量文件）
        });

        // 🔥 用户发送消息后强制滚动到底部
        this.$nextTick(() => {
          this.forceScrollToBottom();
        });
      }

      this.isLoading = true;

      // 准备文件参数和输入参数
      const files = []; // 用于 Chatflow 文件上传（sys.files）
      const inputs = {}; // 用于工作流变量

      // 处理 Chatflow 文件上传（重新思考时不会重复上传附件）
      if (!fromRethink && attachmentsToUpload.length > 0) {
        try {
          for (const file of attachmentsToUpload) {
            if (DEBUG_STREAM_LOG) {
              console.log(
                "开始处理 Chatflow 文件上传:=============================="
              );
            }
            const uploadResult = await uploadFile(
              file.raw,
              this.userId,
              this.appName
            );
            if (DEBUG_STREAM_LOG) {
              console.log(
                "uploadResult:==============================",
                uploadResult
              );
            }
            const difyFileId = this.extractDifyFileId(uploadResult);
            if (DEBUG_STREAM_LOG) {
              console.log(
                "difyFileId:==============================",
                difyFileId
              );
            }
            if (!difyFileId) {
              throw new Error("无法获取文件 ID");
            }
            files.push({
              type: this.getFileType(file.name),
              transfer_method: "local_file",
              upload_file_id: difyFileId,
            });
          }
          if (DEBUG_STREAM_LOG) {
            console.log(
              "📤 已上传 " + attachmentsToUpload.length + " 个文件到 sys.files"
            );
            console.log("📤 files 参数格式:", JSON.stringify(files, null, 2));
          }
          // 文件上传成功，附件列表已在开始时清空
        } catch (error) {
          this.$message.error("文件上传失败：" + error.message);
          this.isLoading = false;
          return;
        }
      }

      // 处理变量输入（包括文件变量）
      // 变量输入在重新思考时仍然沿用当前面板中的值，因此这里不区分 fromRethink
      if (this.appConfig.variables.length > 0) {
        try {
          for (const variable of this.appConfig.variables) {
            const varName = variable.variable || variable.name;
            const value = this.variableValues[varName];

            // 处理文件类型变量
            if (variable.type === "file" || variable.type === "file-list") {
              if (value) {
                if (Array.isArray(value)) {
                  // file-list
                  const uploadedFiles = [];
                  for (const file of value) {
                    const uploadResult = await uploadFile(
                      file.raw,
                      this.userId,
                      this.appName
                    );

                    const difyFileId = this.extractDifyFileId(uploadResult);

                    if (!difyFileId) {
                      throw new Error(`无法获取文件 ID: ${file.name}`);
                    }
                    uploadedFiles.push({
                      type: this.getFileType(file.name),
                      transfer_method: "local_file",
                      upload_file_id: difyFileId,
                    });
                  }
                  inputs[varName] = uploadedFiles;
                } else {
                  // single file
                  const uploadResult = await uploadFile(
                    value.raw,
                    this.userId,
                    this.appName
                  );
                  const difyFileId = this.extractDifyFileId(uploadResult);
                  if (!difyFileId) {
                    throw new Error(`无法获取文件 ID: ${value.name}`);
                  }
                  inputs[varName] = {
                    type: this.getFileType(value.name),
                    transfer_method: "local_file",
                    upload_file_id: difyFileId,
                  };
                }
              }
            } else {
              // 处理其他类型变量
              if (value !== null && value !== undefined && value !== "") {
                inputs[varName] = value;
              }
            }
          }

          if (DEBUG_STREAM_LOG) {
            console.log("📤 变量输入:", Object.keys(inputs).join(", "));
            console.log("📤 inputs 参数格式:", JSON.stringify(inputs, null, 2));
          }
        } catch (error) {
          this.$message.error("变量处理失败：" + error.message);
          this.isLoading = false;
          return;
        }
      }

      // 准备 AI 消息占位
      let aiMessageIndex = this.messages.length;
      this.messages.push({
        role: "assistant",
        content: "",
        thinkingContent: "", // 🔥 思考过程内容
        answerContent: "", // 🔥 回答内容
        displayedAnswerContent: "", // 🔥 显示的回答内容（用于打字效果）
        thinkingLocked: false, // 🔥 思考过程是否已锁定
        typingTimer: null, // 🔥 打字效果定时器
        partialAnswer: "", // 🔥 用于缓冲本次回复的全部 answer 文本
        isStreamFinished: false, // 🔥 是否已经进入最终渲染阶段（服务端流结束 且 打字效果完成）
        serverStreamDone: false, // 🔥 服务端 SSE 流是否已结束（onEnd / stop / error）
        thinkEndIndexRaw: null, // 🔥 思考内容在原始文本中的结束位置（</think> 的起始下标）
        created_at: currentTime + 1,
        messageId: `temp-assistant-${currentTime}`,
      });

      // 创建 AbortController
      this.abortController = new AbortController();

      // 临时存储当前消息的思考内容
      let currentThoughts = [];
      let currentFiles = [];

      // 准备请求参数
      const requestParams = {
        query: userMessage,
        user: this.userId,
        conversation_id: this.currentConversationId,
        signal: this.abortController.signal,
        appName: this.appName, // 传递 App名称
      };

      // 处理 files（Chatflow 文件上传）
      if (files.length > 0) {
        requestParams.files = files;
      }

      // 处理 inputs（工作流变量）
      if (!this.currentConversationId) {
        // 新会话 - 传递所有变量
        if (Object.keys(inputs).length > 0) {
          requestParams.inputs = inputs;
          this.conversationInputs = JSON.parse(JSON.stringify(inputs));
          console.log("📤 新会话 - 传递变量到 inputs，并保存供后续使用");
        } else if (this.appConfig.variables.length > 0) {
          requestParams.inputs = {};
          console.log("📤 新会话 - 传递空 inputs");
        }
      } else {
        // 续接会话 - 继续使用之前的变量
        if (this.appConfig.variables.length > 0) {
          if (Object.keys(inputs).length > 0) {
            // 用户修改了变量
            requestParams.inputs = inputs;
            this.conversationInputs = JSON.parse(JSON.stringify(inputs));
            console.log("📤 续接会话 - 用户修改了变量，使用新值");
          } else if (Object.keys(this.conversationInputs).length > 0) {
            // 使用之前的变量
            requestParams.inputs = this.conversationInputs;
            console.log(
              "📤 续接会话 - 使用之前的变量:",
              this.conversationInputs
            );
          } else {
            requestParams.inputs = {};
            console.log("📤 续接会话 - 无变量可用，传递空 inputs");
          }
        }
      }

      // 打印发送的参数（调试用）
      console.log("📤 最终发送参数:", {
        query: userMessage.substring(0, 50) + "...",
        user: this.userId,
        conversation_id: this.currentConversationId,
        files: requestParams.files,
        inputs: requestParams.inputs,
        hasFiles: !!requestParams.files,
        hasInputs: !!requestParams.inputs,
        filesLength: requestParams.files?.length,
        inputsKeys: requestParams.inputs
          ? Object.keys(requestParams.inputs)
          : undefined,
      });

      // 发送消息
      this.streamController = sendChatMessage(
        requestParams,
        // onMessage
        (data) => {
          // 调试：打印每一条流式事件的原始数据（包含 answer 增量）
          if (DEBUG_STREAM_LOG) {
            console.log(
              "%c[STREAM][RAW]",
              "color:#2563eb;font-weight:bold;",
              {
                event: data.event,
                // 当前增量 answer（可能是 1 个字、几个字或一整段）
                answerChunk: data.answer,
                answerChunkLength: data.answer ? data.answer.length : 0,
                // 是否疑似包含表格（管道符）
                hasTableChar:
                  typeof data.answer === "string" &&
                  data.answer.includes("|"),
                // 是否疑似包含思考标签 /think
                hasThinkTag:
                  typeof data.answer === "string" &&
                  (data.answer.includes("<think>") ||
                    data.answer.includes("</think>")),
                // 是否疑似包含 HTML 标签
                hasHtmlTag:
                  typeof data.answer === "string" &&
                  /<[^>]+>/.test(data.answer),
                // 结构化思考
                thought: data.thought,
                tool: data.tool,
                observation: data.observation,
              }
            );
          }

          // 保存 task_id
          if (data.task_id && !this.currentTaskId) {
            this.currentTaskId = data.task_id;
          }

          if (!this.messages[aiMessageIndex]) return;

          // 根据事件类型处理
          switch (data.event) {
            case "agent_thought":
              // Agent 思考事件
              if (data.thought) {
                if (DEBUG_STREAM_LOG) {
                  console.log(
                    "%c[STREAM][THINK]",
                    "color:#8b5cf6;font-weight:bold;",
                    {
                      id: data.id,
                      thoughtPreview: data.thought.substring(0, 80),
                      thoughtLength: data.thought.length,
                      tool: data.tool,
                      observationPreview: data.observation
                        ? data.observation.substring(0, 80)
                        : "",
                    }
                  );
                }

                const existingThought = currentThoughts.find(
                  (t) => t.id === data.id
                );
                if (!existingThought) {
                  currentThoughts.push({
                    id: data.id,
                    thought: data.thought,
                    tool: data.tool || "",
                    observation: data.observation || "",
                    position: data.position || currentThoughts.length + 1,
                  });
                } else {
                  // 更新已有的思考
                  Object.assign(existingThought, {
                    thought: data.thought,
                    tool: data.tool || existingThought.tool,
                    observation:
                      data.observation || existingThought.observation,
                  });
                }
              }
              break;

            case "message_file":
              // 文件事件
              if (data.file) {
                const existingFile = currentFiles.find(
                  (f) => f.id === data.file.id
                );
                if (!existingFile) {
                  currentFiles.push(data.file);
                }
              }
              break;

            case "message":
            case "agent_message":
              // 常规消息事件 - answer 是增量的
              if (data.answer) {
                // 将新的内容追加到消息中（但要重新构建完整内容）
                this.messages[aiMessageIndex].partialAnswer =
                  (this.messages[aiMessageIndex].partialAnswer || "") +
                  data.answer;

                if (DEBUG_STREAM_LOG) {
                  const fullLen =
                    this.messages[aiMessageIndex].partialAnswer.length;
                  console.log(
                    "%c[STREAM][ANSWER-CHUNK]",
                    "color:#16a34a;font-weight:bold;",
                    {
                      chunkLength: data.answer.length,
                      cumulativeLength: fullLen,
                      chunkPreview: data.answer.substring(0, 80),
                    }
                  );
                }
              }
              break;
          }

          // 🔥 流式更新策略（改进版）：
          // 1. 流式阶段：记录 </think> 在原始文本中的位置 thinkEndIndexRaw，不立即拆分
          // 2. 在打字过程中，当显示长度 >= thinkEndIndexRaw 时，再将之前内容移入思考框体
          // 3. 一旦思考块已锁定（thinkingLocked = true），answerContent 只保留 </think> 之后的正式回答，避免思考内容在回答中重复出现
          // 4. 最终阶段（流结束 + 打字结束）再调用 finalizeThinkingForMessage 做一次兜底拆分，处理历史记录等场景
          const rawAnswer = this.messages[aiMessageIndex].partialAnswer || "";
          const msg = this.messages[aiMessageIndex];

          // 首次检测到 </think> 时，只记录其在原始文本中的位置，不立刻拆分
          if (msg.thinkEndIndexRaw == null) {
            const endIndex = rawAnswer.indexOf("</think>");
            if (endIndex !== -1) {
              msg.thinkEndIndexRaw = endIndex;
            }
          }

          // 展示用的 answerContent：
          // - 如果思考块尚未锁定：完整展示（仅移除 </think> 标签本身）
          // - 如果思考块已锁定：只保留 </think> 之后的部分，避免思考内容在回答区域重复出现
          if (msg.thinkingLocked && msg.thinkEndIndexRaw != null) {
            const afterEnd = rawAnswer
              .slice(msg.thinkEndIndexRaw + "</think>".length)
              .trim()
              .replace(/^\s*\n+/, "");
            msg.answerContent = afterEnd.replace(/<\/think>/g, "");
          } else {
            msg.answerContent = rawAnswer.replace(/<\/think>/g, "");
          }
          // 🔥 确保 displayedAnswerContent 已初始化（空字符串），以便模板能正确显示打字效果
          if (
            msg.displayedAnswerContent === undefined ||
            msg.displayedAnswerContent === null
          ) {
            msg.displayedAnswerContent = "";
          }
          // 🔥 触发打字效果（目标为当前展示用回答内容）
          this.startTypingEffect(aiMessageIndex, msg.answerContent || "");

          // 构建完整内容用于兼容性（某些地方可能还在用 content）
          let fullContent = "";
          if (msg.thinkingContent && msg.thinkingLocked) {
            fullContent = `<think>${msg.thinkingContent}</think>`;
          }
          if (msg.answerContent) {
            fullContent += msg.answerContent;
          }

          // 添加文件
          if (currentFiles.length > 0) {
            fullContent += "\n\n";
            currentFiles.forEach((file) => {
              if (file.type === "image") {
                fullContent += `\n<img src="${file.url}" alt="生成的图片" style="max-width: 100%; border-radius: 8px; margin-top: 8px;" />`;
              } else {
                // 尝试多种方式获取文件名
                let fileName = file.name || file.filename || file.title;

                // 尝试从 belongs_to 字段获取
                if (!fileName && file.belongs_to) {
                  fileName = file.belongs_to.name || file.belongs_to.filename;
                }

                // 尝试从 extension_data 获取
                if (!fileName && file.extension_data) {
                  fileName =
                    file.extension_data.name || file.extension_data.filename;
                }

                // 尝试从 URL 查询参数中获取
                if (!fileName && file.url) {
                  try {
                    const urlObj = new URL(file.url);
                    // 检查查询参数
                    const nameParam =
                      urlObj.searchParams.get("name") ||
                      urlObj.searchParams.get("filename") ||
                      urlObj.searchParams.get("file_name");
                    if (nameParam) {
                      fileName = decodeURIComponent(nameParam);
                    } else {
                      // 尝试从路径提取
                      const pathParts = urlObj.pathname.split("/");
                      const lastPart = pathParts[pathParts.length - 1];
                      if (lastPart && lastPart !== "file-preview") {
                        fileName = decodeURIComponent(lastPart);
                      }
                    }
                  } catch (e) {
                    console.warn("无法从 URL 提取文件名:", e);
                  }
                }

                // 最后的回退选项
                if (!fileName || fileName === "file-preview") {
                  fileName = `${file.type || "document"}文件`;
                }

                fullContent += `\n<div class="file-link-wrapper"><i class="el-icon-document"></i> <a href="${file.url}" target="_blank" class="file-link">${fileName}</a></div>`;
              }
            });
          }

          // 更新消息内容
          this.messages[aiMessageIndex].content = fullContent;

          // 🔥 保持 isLoading = true 直到流式结束，确保停止按钮可用

          this.$nextTick(() => {
            this.scrollToBottom();
          });
        },
        // onEnd
        (data) => {
          console.log("🏁 [onEnd] 流式响应结束，完整数据:", data);

          if (DEBUG_STREAM_LOG) {
            console.log(
              "%c[STREAM][END]",
              "color:#ea580c;font-weight:bold;",
              {
                conversation_id: data.conversation_id,
                // 最终 metadata（包含引用文档、推荐问题等）
                metadata: data.metadata,
                // Dify 生成的文件信息（图片、文档等）
                files: data.files,
              }
            );
          }

          this.currentTaskId = null;
          this.abortController = null;
          this.streamController = null;
          this.attachments = [];

          // 🔥 确保打字效果完成，显示所有内容
          if (this.messages[aiMessageIndex]) {
            const msg = this.messages[aiMessageIndex];
            // 标记服务端流已结束（但不立刻打断打字效果）
            msg.serverStreamDone = true;

            // 如果此时已经没有打字定时器，说明内容已经全部展示，可以直接进入最终渲染
            if (!msg.typingTimer) {
              msg.isStreamFinished = true;
              // 流式和打字都结束了，可以彻底结束 loading 状态
              this.isLoading = false;
              if (msg.answerContent) {
                msg.displayedAnswerContent = msg.answerContent;
              }
            }
          }

          // 重置临时变量
          currentThoughts = [];
          currentFiles = [];

          // 保存 conversation_id
          if (data.conversation_id && !this.currentConversationId) {
            this.currentConversationId = data.conversation_id;
            this.loadConversationsList();
          }

          // 处理返回的文件（data.files）
          if (data.files && Array.isArray(data.files) && data.files.length > 0) {
            console.log("📎 [onEnd] 开始处理文件，共", data.files.length, "个");
            let filesHtml = "\n\n";

            data.files.forEach((file) => {
              // 获取文件名
              const fileName = file.filename || file.name || "下载文件";

              // 转换文件 URL：将 Dify 内部地址转换为可访问的代理地址
              let fileUrl = file.url || "";

              // 智能检测是否需要通过后端代理
              const needsProxy = this.isInternalUrl(fileUrl);

              if (needsProxy) {
                // 提取 /files/ 之后的路径
                const match = fileUrl.match(/\/files\/(.+)$/);
                if (match) {
                  const filePath = match[1];
                  const baseURL = process.env.VUE_APP_BASE_API || '/api';
                  fileUrl = `${baseURL}/api/ai/chat/files/proxy/${filePath}`;
                  console.log(`📎 [onEnd] 内部地址检测到，使用代理: ${file.url} -> ${fileUrl}`);
                } else {
                  console.warn(`⚠️ [onEnd] 无法提取文件路径: ${fileUrl}`);
                }
              } else {
                console.log(`🌐 [onEnd] 公网地址，直接使用: ${fileUrl}`);
              }

              // 根据文件类型渲染
              if (file.type === "image") {
                filesHtml += `\n<img src="${fileUrl}" alt="${fileName}" style="max-width: 100%; border-radius: 8px; margin-top: 8px;" />`;
              } else {
                // 获取文件扩展名和格式化大小
                const fileExt = fileName.split('.').pop().toLowerCase();
                const fileSize = file.size ? this.formatFileSize(file.size) : '';
                const fileIcon = this.getFileIcon(fileExt);

                // 文档类型的文件显示为美观的卡片
                const cardId = `file-card-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
                filesHtml += `\n<div class="file-card" data-card-id="${cardId}"><div class="file-icon"><i class="el-icon-${fileIcon}"></i><span class="file-ext">.${fileExt}</span></div><div class="file-info"><div class="file-name" title="${fileName}">${fileName}</div>${fileSize ? `<div class="file-size">${fileSize}</div>` : ''}</div><a href="${fileUrl}" download="${fileName}" class="file-download" title="下载文件"><i class="el-icon-download"></i></a></div>`;
              }
            });

            // 直接追加文件到内容中
            if (this.messages[aiMessageIndex]) {
              const msg = this.messages[aiMessageIndex];
              msg.answerContent = (msg.answerContent || "") + filesHtml;
              msg.displayedAnswerContent = (msg.displayedAnswerContent || "") + filesHtml;
              msg.content = (msg.content || "") + filesHtml;
              console.log("✅ [onEnd] 文件已追加到回答内容");
            }
          }

          // 处理引用和归属
          if (
            data.metadata?.retriever_resources &&
            data.metadata.retriever_resources.length > 0
          ) {
            const retrieverResources = data.metadata.retriever_resources;
            let referencesHtml = '\n\n<div class="references-section">';
            referencesHtml +=
              '<div class="references-title"><i class="el-icon-link"></i> 引用和归属</div>';
            retrieverResources.forEach((resource, idx) => {
              referencesHtml += `<div class="reference-item">`;
              referencesHtml += `<div class="reference-header">`;
              referencesHtml += `<span class="reference-index">${
                idx + 1
              }</span>`;
              referencesHtml += `<span class="reference-dataset">${resource.dataset_name}</span>`;
              referencesHtml += `<span class="reference-score">相似度: ${(
                resource.score * 100
              ).toFixed(1)}%</span>`;
              referencesHtml += `</div>`;
              referencesHtml += `<div class="reference-content">${
                resource.content?.substring(0, 200) || ""
              }...</div>`;
              referencesHtml += `</div>`;
            });
            referencesHtml += "</div>";

            // 添加到最后一条 AI 消息
            if (this.messages[aiMessageIndex]) {
              this.messages[aiMessageIndex].content += referencesHtml;
              this.messages[aiMessageIndex].hasReferences = true;
            }
          }

          // 处理下一步问题建议
          if (
            this.appConfig.suggestedQuestionsAfterAnswer &&
            data.metadata?.suggested_questions
          ) {
            this.currentSuggestedQuestions = data.metadata.suggested_questions;
          } else {
            this.currentSuggestedQuestions = [];
          }

          this.$nextTick(() => {
            this.scrollToBottom();
            // 🔥 默认展开思考过程，不再自动折叠
            // this.autoCollapseThinkBlocks()
          });
        },
        // onError
        (error) => {
          this.isLoading = false;
          this.currentTaskId = null;
          this.abortController = null;
          this.streamController = null;

          // 🔥 清除打字效果定时器
          if (this.messages[aiMessageIndex]) {
            const msg = this.messages[aiMessageIndex];
            if (msg.typingTimer) {
              clearInterval(msg.typingTimer);
              msg.typingTimer = null;
            }
            // 视为当前流已终止，直接使用最终渲染
            msg.serverStreamDone = true;
            msg.isStreamFinished = true;
            // 确保显示内容等于完整内容
            if (msg.answerContent) {
              msg.displayedAnswerContent = msg.answerContent;
            }
          }

          // 重置临时变量
          currentThoughts = [];
          currentFiles = [];

          // 如果是 AbortError，说明是用户主动停止，不显示错误消息
          if (error.name !== "AbortError") {
            this.$message.error("发送失败：" + (error.message || "未知错误"));
          } else {
            console.log("✅ 用户已停止生成");
          }
        }
      );
    },

    handleKeydown(e) {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        this.handleSend();
      }
    },

    async stopGeneration() {
      if (!this.isLoading) {
        this.$message.warning("没有正在进行的任务");
        return;
      }

      // 区分两种场景：
      // 1) 服务端流仍在进行：真正“停止生成”（中断 SSE + 调用停止 API）
      // 2) 服务端流已结束，仅前端还有打字动画：视为“全部输出”（跳过动画，直接展示全部内容）

      // 找到当前这一条正在渲染的助手消息
      let currentAssistant = null;
      for (let i = this.messages.length - 1; i >= 0; i--) {
        const msg = this.messages[i];
        if (msg.role === "assistant" && !msg.isStreamFinished) {
          currentAssistant = msg;
          break;
        }
      }

      const isFastForwardOnly =
        currentAssistant && currentAssistant.serverStreamDone;

      if (isFastForwardOnly) {
        // ✅ 场景2：服务端数据已经全部返回，只是还有打字动画
        // 只做前端“全部输出”，不再调用后端停止接口
        this.messages.forEach((msg) => {
          if (msg.typingTimer) {
            clearInterval(msg.typingTimer);
            msg.typingTimer = null;
          }
          if (msg.role === "assistant") {
            msg.serverStreamDone = true;
            msg.isStreamFinished = true;
            // 显示完整内容
            if (msg.answerContent) {
              msg.displayedAnswerContent = msg.answerContent;
            }
          }
        });

        this.isLoading = false;
        this.currentTaskId = null;
        this.abortController = null;
        this.streamController = null;

        this.$message.success("已全部输出");
        return;
      }

      // ✅ 场景1：真正停止生成（服务端仍在推流）
      try {
        // 1. 中断 fetch 请求
        if (this.abortController) {
          try {
            this.abortController.abort();
          } catch (e) {
            // AbortError 是正常的，忽略
          }
        }

        // 2. 取消 stream reader
        if (this.streamController && this.streamController.abort) {
          try {
            this.streamController.abort();
          } catch (e) {
            // AbortError 是正常的，忽略
          }
        }

        // 3. 调用 Dify 停止 API（如果有 task_id）
        if (this.currentTaskId) {
          try {
            await stopChatMessage(this.currentTaskId, this.userId);
          } catch (apiError) {
            // 忽略停止 API 的错误
          }
        }

        // 4. 清理状态
        this.isLoading = false;
        this.currentTaskId = null;
        this.abortController = null;
        this.streamController = null;

        // 🔥 清除所有消息的打字效果定时器，并标记为已结束，使用最终渲染
        this.messages.forEach((msg) => {
          if (msg.typingTimer) {
            clearInterval(msg.typingTimer);
            msg.typingTimer = null;
          }
          if (msg.role === "assistant") {
            msg.serverStreamDone = true;
            msg.isStreamFinished = true;
          }
          // 确保显示内容等于完整内容
          if (
            msg.answerContent &&
            msg.displayedAnswerContent !== msg.answerContent
          ) {
            msg.displayedAnswerContent = msg.answerContent;
          }
        });

        this.$message.success("已停止生成");
      } catch (error) {
        // 捕获并忽略 AbortError
        if (error.name !== "AbortError") {
          console.error("停止失败:", error);
        }

        // 无论如何都要清理状态
        this.isLoading = false;
        this.currentTaskId = null;
        this.abortController = null;
        this.streamController = null;

        this.$message.info("已终止");
      }
    },

    /**
     * 🔥 格式化思考过程块（一旦锁定就不再变化，避免闪烁）
     * @param {string} thinkingContent - 思考内容
     * @param {string} messageId - 消息ID，用于生成稳定的块ID
     */
    formatThinkingBlock(thinkingContent, messageId) {
      if (!thinkingContent) return "";

      // 使用消息ID生成稳定的思考块ID
      const thinkId = `think-${messageId}`;

      // 格式化思考内容
      const formatted = this.formatTextContent(thinkingContent, "main", null);

      return `<div class="think-block">
        <div class="think-header" onclick="toggleThinkBlock('${thinkId}')">
          <i class="el-icon-view"></i>
          <span>思考过程</span>
          <i class="el-icon-arrow-up think-toggle-icon" id="icon-${thinkId}"></i>
        </div>
        <div class="think-content" id="content-${thinkId}">${formatted}</div>
      </div>`;
    },

    /**
     * 🔥 格式化回答内容
     * @param {string} answerContent - 回答内容
     * @param {boolean} isFinal - 是否为最终渲染（流式结束后）
     *
     * 设计约定：
     * - 流式过程中（isFinal = false）：不做 Markdown 表格解析，表格以原始 markdown 文本形式显示，避免半成品表格闪烁
     * - 流式结束后（isFinal = true）：对完整文本做一次富渲染，解析表格/段落等
     */
    formatAnswerContent(answerContent, isFinal = false) {
      if (!answerContent) return "";
      return this.formatTextContent(answerContent, "main", null, {
        streaming: !isFinal,
      });
    },

    /**
     * 🔥 打字效果：逐字显示内容
     * @param {number} messageIndex - 消息索引
     * @param {string} targetContent - 目标内容
     */
    startTypingEffect(messageIndex, targetContent) {
      const msg = this.messages[messageIndex];
      if (!msg) return;

      // 如果目标内容为空，直接清空显示内容
      if (!targetContent) {
        if (msg.typingTimer) {
          clearInterval(msg.typingTimer);
          msg.typingTimer = null;
        }
        msg.displayedAnswerContent = "";
        // 如果服务端已经结束，也视为完成最终渲染
        if (msg.serverStreamDone) {
          msg.isStreamFinished = true;
          // 🔥 最终阶段再统一拆分思考 / 回答，构建思考框体
          this.finalizeThinkingForMessage(msg);
          this.isLoading = false;
        }
        return;
      }

      // 初始化显示内容
      if (!msg.displayedAnswerContent) {
        msg.displayedAnswerContent = "";
      }

      // 获取当前显示内容的长度
      const currentLength = msg.displayedAnswerContent.length;
      const targetLength = targetContent.length;

      // 如果显示内容已经等于或超过目标内容，不需要打字效果
      if (currentLength >= targetLength) {
        // 如果目标内容变短了（可能是内容被替换），直接更新
        if (targetLength < currentLength) {
          if (msg.typingTimer) {
            clearInterval(msg.typingTimer);
            msg.typingTimer = null;
          }
          msg.displayedAnswerContent = targetContent;
        }
        // 已经展示完整内容，如果服务端流也结束了，则进入最终渲染
        if (msg.serverStreamDone) {
          msg.isStreamFinished = true;
          // 🔥 最终阶段再统一拆分思考 / 回答，构建思考框体
          this.finalizeThinkingForMessage(msg);
          this.isLoading = false;
        }
        return;
      }

      // 如果已经有定时器在运行，不需要重新创建
      if (msg.typingTimer) {
        return;
      }

      // 逐字显示
      const typingSpeed = 20; // 每个字符的延迟（毫秒）

      msg.typingTimer = setInterval(() => {
        // 重新获取最新的目标内容（因为流式数据可能还在更新）
        const latestTarget = msg.answerContent || "";
        const latestCurrentLength = msg.displayedAnswerContent.length;
        const latestTargetLength = latestTarget.length;

        // 如果目标内容已经完成，直接显示全部
        if (latestCurrentLength >= latestTargetLength) {
          msg.displayedAnswerContent = latestTarget;
          if (msg.typingTimer) {
            clearInterval(msg.typingTimer);
            msg.typingTimer = null;
          }
          // 打字效果完成，如果服务端流已结束，则此时触发最终渲染（表格/HTML 解析）
          if (msg.serverStreamDone) {
            msg.isStreamFinished = true;
            // 🔥 最终阶段再统一拆分思考 / 回答，构建思考框体
            this.finalizeThinkingForMessage(msg);
            this.isLoading = false;
          }
          return;
        }

        // 显示下一个字符
        msg.displayedAnswerContent = latestTarget.substring(0, latestCurrentLength + 1);

        // 🔥 在打字过程中检查是否“已经输出到 </think> 前”，如果是则立即将之前内容移入思考框体
        this.maybeFinalizeThinkingDuringTyping(messageIndex);

        // 滚动到底部
        this.$nextTick(() => {
          this.scrollToBottom();
        });
      }, typingSpeed);
    },


    formatMessage(content) {
      if (!content) return "";

      let result = "";
      let thinkBlockCount = 0;

      // 🔥 新的识别策略：兼容多种格式
      // 格式1: <think>思考内容</think>正式回答
      // 格式2: 思考内容</think>\n\n正式回答（缺少开始标签）
      // 格式3: 纯思考内容（无标签，通过特征识别）

      // 步骤1：检查是否包含 </think> 结束标签
      const hasEndTag = content.includes("</think>");

      if (hasEndTag) {
        // 场景A：包含 </think> 标签
        // 将 </think> 之前的所有内容视为思考过程
        const parts = content.split("</think>");

        // 处理思考部分（可能包含或不包含 <think> 开始标签）
        let thinkContent = parts[0];
        // 移除可能存在的 <think> 开始标签
        thinkContent = thinkContent.replace(/^<think>/i, "").trim();

        if (thinkContent) {
          thinkBlockCount++;
          result += this.formatTextContent(
            thinkContent,
            "think",
            thinkBlockCount
          );
        }

        // 处理回答部分（</think> 之后的内容）
        if (parts.length > 1) {
          // 合并剩余部分（可能有多个 </think>）
          const answerContent = parts.slice(1).join("").trim();
          // 清理开头的多余换行（通常是 \n\n）
          const cleanAnswer = answerContent.replace(/^\s*\n+/, "");

          if (cleanAnswer) {
            result += this.formatTextContent(cleanAnswer, "main", null);
          }
        }
      } else {
        // 场景B：不包含标签，尝试智能识别
        // 检测是否为纯思考内容（通过特征词判断）
        const isThinkingContent = this.detectThinkingContent(content);

        if (isThinkingContent && content.length > 100) {
          // 可能是纯思考过程
          thinkBlockCount++;
          result += this.formatTextContent(content, "think", thinkBlockCount);
        } else {
          // 正常回答内容
          result += this.formatTextContent(content, "main", null);
        }
      }

      return result || this.formatTextContent(content, "main", null);
    },

    parseMarkdownTable(tableLines) {
      if (!tableLines || tableLines.length < 2) return null;

      // 解析表格行
      const parseRow = (line) => {
        return line
          .split("|")
          .map((cell) => {
            // 去除前后空格和 Markdown 粗体标记 **
            let cleanCell = cell.trim();
            // 移除粗体标记 **text** -> text（处理整个单元格被包裹的情况）
            cleanCell = cleanCell.replace(/^\*\*(.*?)\*\*$/g, "$1");
            // 移除单元格内所有的粗体标记
            cleanCell = cleanCell.replace(/\*\*(.*?)\*\*/g, "$1");
            return cleanCell;
          })
          .filter((cell, idx, arr) => {
            // 过滤掉首尾的空单元格（由于 | 开头/结尾产生）
            return (
              !(idx === 0 && cell === "") &&
              !(idx === arr.length - 1 && cell === "")
            );
          });
      };

      const rows = tableLines.map((line) => parseRow(line));

      // 检查是否有分隔符行（第二行通常是 |---|---|---| 格式）
      let hasHeaderSeparator = false;
      let headerRowIndex = 0;
      let dataStartIndex = 1;

      if (rows.length >= 2) {
        const secondRow = rows[1];
        // 检查是否为分隔符行（包含 - 或 : 字符）
        if (secondRow.every((cell) => /^:?-+:?$/.test(cell))) {
          hasHeaderSeparator = true;
          dataStartIndex = 2;
        }
      }

      // 如果所有行的列数不一致，可能不是有效的表格
      const columnCount = rows[0].length;
      const validRows = rows.filter(
        (row) => row.length === columnCount || /^:?-+:?$/.test(row[0])
      );

      if (validRows.length < 2) return null;

      // 构建HTML表格
      let tableHtml =
        '<div class="markdown-table-wrapper"><table class="markdown-table">';

      // 表头
      if (hasHeaderSeparator) {
        tableHtml += "<thead><tr>";
        rows[headerRowIndex].forEach((cell) => {
          tableHtml += `<th>${cell}</th>`;
        });
        tableHtml += "</tr></thead>";
      }

      // 表体
      tableHtml += "<tbody>";
      for (let i = dataStartIndex; i < rows.length; i++) {
        // 跳过分隔符行
        if (rows[i].every((cell) => /^:?-+:?$/.test(cell))) continue;

        // 只处理列数匹配的行
        if (rows[i].length !== columnCount) continue;

        tableHtml += "<tr>";
        rows[i].forEach((cell, cellIdx) => {
          // 如果没有表头，第一行也作为数据行
          const tag = !hasHeaderSeparator && i === 0 ? "th" : "td";
          tableHtml += `<${tag}>${cell}</${tag}>`;
        });
        tableHtml += "</tr>";
      }
      tableHtml += "</tbody></table></div>";

      return tableHtml;
    },

    formatTextContent(text, type = "main", thinkIndex = null, options = {}) {
      if (!text) return "";

      const isStreaming = options && options.streaming === true;

      // 预处理 Markdown 标题：将类似 "#标题" 规范成 "# 标题"
      // 这样可以兼容模型输出中 "#一级标题"、"##二级标题" 这类省略空格的情况
      text = text.replace(/^(#{1,6})([^#\s])(.*)$/gm, "$1 $2$3");

      // 思考内容：移除重复段落，避免重复占用空间
      if (type === "think") {
        const paragraphs = text
          .split(/\n{2,}/)
          .map((p) => p.trim())
          .filter(Boolean);
        const seen = new Set();
        const deduped = [];
        for (const p of paragraphs) {
          const key = p.replace(/\s+/g, " ");
          if (!seen.has(key)) {
            seen.add(key);
            deduped.push(p);
          }
        }
        text = deduped.join("\n\n");
      }

      // 0. 移除可能残留的 think 标签
      let formatted = text.replace(/<\/?think>/gi, "");

      // 0.1 移除 Markdown 格式的文件链接（Dify 返回的文件链接）
      // 匹配格式: [filename](http://xxx/files/...)
      // 这些链接会被替换为文件卡片，所以原始的 Markdown 链接可以移除
      formatted = formatted.replace(/\[([^\]]+)\]\(http[s]?:\/\/[^\/\s]+\/files\/[^\)]+\)/g, '');

      // 1. 先提取并保存 HTML 标签，避免被转义
      const htmlPlaceholders = [];

      // 保存文件卡片（新样式）- 使用 data-card-id 属性精确匹配
      formatted = formatted.replace(
        /<div class="file-card" data-card-id="[^"]*">.*?<\/a><\/div>/gs,
        (match) => {
          const index = htmlPlaceholders.length;
          htmlPlaceholders.push(match);
          return `__HTML_PLACEHOLDER_${index}__`;
        }
      );

      // 保存文件链接包装器（旧样式兼容）
      formatted = formatted.replace(
        /<div class="file-link-wrapper">[\s\S]*?<\/div>/g,
        (match) => {
          const index = htmlPlaceholders.length;
          htmlPlaceholders.push(match);
          return `__HTML_PLACEHOLDER_${index}__`;
        }
      );

      // 保存图片标签
      formatted = formatted.replace(/<img[^>]*>/g, (match) => {
        const index = htmlPlaceholders.length;
        htmlPlaceholders.push(match);
        return `__HTML_PLACEHOLDER_${index}__`;
      });
      // 保存单独的链接标签
      formatted = formatted.replace(/<a[^>]*>.*?<\/a>/g, (match) => {
        const index = htmlPlaceholders.length;
        htmlPlaceholders.push(match);
        return `__HTML_PLACEHOLDER_${index}__`;
      });

      // 2. 处理代码块（在转义前处理）
      // 规则优化：只有“真正的代码块”才使用黑色背景的特殊展示；
      // 对于 markdown/doc 等语言，只去掉 ``` 包裹，让内部内容按普通文本/markdown 渲染。
      const codeBlockPlaceholders = [];
      formatted = formatted.replace(
        /```(\w+)?\n?([\s\S]*?)```/g,
        (match, lang, code) => {
          const language = (lang || "").toLowerCase();
          const nonCodeLangs = ["markdown", "md", "text", "txt", "plain"];

          // 对于 markdown / 文本类块：直接还原内部内容，不使用代码黑框
          if (!language || nonCodeLangs.includes(language)) {
            return code.trim();
          }

          // 真正的代码块（html / js / css / 等）仍然用黑框展示
          const index = codeBlockPlaceholders.length;
          const escapedCode = code
            .trim()
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
          codeBlockPlaceholders.push(
            `<pre class="code-block"><code>${escapedCode}</code></pre>`
          );
          return `__CODE_BLOCK_${index}__`;
        }
      );

      // 3. 处理行内代码（在转义前处理）
      const inlineCodePlaceholders = [];
      formatted = formatted.replace(/`([^`]+)`/g, (match, code) => {
        const index = inlineCodePlaceholders.length;
        const escapedCode = code
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");
        inlineCodePlaceholders.push(
          `<code class="inline-code">${escapedCode}</code>`
        );
        return `__INLINE_CODE_${index}__`;
      });

      // 4. 转义普通文本中的 HTML 标签
      formatted = formatted
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");

      // 4.1 精简空行：去掉首尾空行（保留中间的多余空行用于段落分隔）
      formatted = formatted
        .replace(/^\n+/, "")
        .replace(/\n+$/, "");

      // 4.2 处理 Markdown 表格（在换行替换之前处理）
      // 流式过程中不解析表格，避免半成品表格频繁重排；只在最终渲染时解析
      const tablePlaceholders = [];
      if (!isStreaming) {
        const lines = formatted.split("\n");
        let i = 0;
        while (i < lines.length) {
          const line = lines[i].trim();
          // 检测表格行（包含 | 符号）
          if (line.includes("|") && line.split("|").length >= 3) {
            const tableLines = [];
            let j = i;
            // 收集连续的表格行
            while (j < lines.length && lines[j].trim().includes("|")) {
              tableLines.push(lines[j].trim());
              j++;
            }

            // 如果至少有2行（表头+分隔符或表头+数据行），则认为是表格
            if (tableLines.length >= 2) {
              const tableHtml = this.parseMarkdownTable(tableLines);
              if (tableHtml) {
                if (DEBUG_STREAM_LOG) {
                  console.log(
                    "%c[FORMAT][TABLE]",
                    "color:#0ea5e9;font-weight:bold;",
                    {
                      rawLines: tableLines,
                      htmlPreview: tableHtml.substring(0, 200),
                    }
                  );
                }
                const index = tablePlaceholders.length;
                tablePlaceholders.push(tableHtml);
                // 将表格行替换为占位符，并标记要删除的行
                lines[i] = `__TABLE_PLACEHOLDER_${index}__`;
                // 删除表格的其他行（从后往前删除）
                for (let k = j - 1; k > i; k--) {
                  lines.splice(k, 1);
                }
                i++;
                continue;
              }
            }
          }
          i++;
        }
        formatted = lines.join("\n");
      }

      // 4.3 将单独一行的 --- / ___ / *** 等视为分隔线，避免界面上看到“---”裸文本
      formatted = formatted.replace(
        /^\s*([-_*])\1{2,}\s*$/gm,
        '<hr class="ai-hr-separator">'
      );

      // 4.4 处理 Markdown 标题（在换行 -> <br> 之前，将 # / ## / ### 转为 h1~h6）
      formatted = formatted.replace(
        /^(#{1,6})\s+(.+)$/gm,
        (match, hashes, title) => {
          const level = Math.min(hashes.length, 6);
          const cleanTitle = title.trim();
          if (!cleanTitle) return cleanTitle;
          return `<h${level}>${cleanTitle}</h${level}>`;
        }
      );

      // 5. 处理 Markdown 语法
      // 处理粗体
      formatted = formatted.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");

      // 处理换行
      formatted = formatted.replace(
        /\n/g,
        '<br style="line-height: 1.6; margin: 0; padding: 0;">'
      );

      // 恢复表格占位符
      tablePlaceholders.forEach((table, index) => {
        formatted = formatted.replace(`__TABLE_PLACEHOLDER_${index}__`, table);
      });

      // 处理列表
      formatted = formatted.replace(/^- (.+)(<br[^>]*>)/gm, "<li>$1</li>$2");
      formatted = formatted.replace(
        /(<li>.*<\/li>(<br[^>]*>)?)+/g,
        "<ul>$&</ul>"
      );

      // 6. 恢复占位符
      // 恢复代码块
      codeBlockPlaceholders.forEach((code, index) => {
        formatted = formatted.replace(`__CODE_BLOCK_${index}__`, code);
      });

      // 恢复行内代码
      inlineCodePlaceholders.forEach((code, index) => {
        formatted = formatted.replace(`__INLINE_CODE_${index}__`, code);
      });

      // 恢复 HTML 标签
      htmlPlaceholders.forEach((html, index) => {
        if (html.includes("file-link-wrapper")) {
          console.log(
            `📎 恢复文件链接 HTML [${index}]:`,
            html.substring(0, 100)
          );
        }
        formatted = formatted.replace(`__HTML_PLACEHOLDER_${index}__`, html);
      });

      // 7. 段落包装：将连续的 <br> 视为段落分隔，并使用 <p> 包裹文本
      // 仅对普通文本内容生效（think/main 两种类型）
      if (type === "main" || type === "think") {
        // 去掉开头和末尾多余的 <br>，避免空段落
        formatted = formatted
          .replace(/^(<br[^>]*>\s*)+/, "")
          .replace(/(<br[^>]*>\s*)+$/, "");

        // 按两次及以上 <br> 切分为段落片段
        const paragraphParts = formatted.split(/(?:<br[^>]*>\s*){2,}/);
        const rebuiltParagraphs = [];

        paragraphParts.forEach((part) => {
          const trimmedPart = part.trim();

          // 如果本段是一个纯标题（<h1>~<h6>），直接保留为标题，不再包裹 <p>
          if (/^<h[1-6]>[\s\S]*<\/h[1-6]>$/.test(trimmedPart)) {
            rebuiltParagraphs.push(trimmedPart);
            return;
          }

          const plainText = part
            .replace(/<br[^>]*>/g, "")
            .replace(/&nbsp;/g, " ")
            .trim();
          if (!plainText) {
            return;
          }

          // 检测是否为「1. xxx」/「1、xxx」这类编号段落，如果是则不做首行缩进
          const isNumbered =
            /^\d+[\.\u3001、]\s*/.test(plainText) ||
            /^[（(]?\d+[）)]/.test(plainText);

          const cls = isNumbered ? ' class="no-indent"' : "";
          rebuiltParagraphs.push(`<p${cls}>${part}</p>`);
        });

        formatted = rebuiltParagraphs.join("");
      }

      // 根据类型包装内容
      if (type === "think") {
        // 🔥 关键修复：使用稳定的ID（基于内容哈希 + 索引）
        // 这样每次渲染相同内容时会生成相同的ID，避免DOM重建
        let stableHash = 0;
        for (let i = 0; i < text.length; i++) {
          stableHash = (stableHash << 5) - stableHash + text.charCodeAt(i);
          stableHash = stableHash & stableHash; // 转为32位整数
        }
        const thinkId = `think-${Math.abs(stableHash)}-${thinkIndex || 0}`;

        return `<div class="think-block">
          <div class="think-header" onclick="toggleThinkBlock('${thinkId}')">
            <i class="el-icon-view"></i>
            <span>思考过程</span>
            <i class="el-icon-arrow-up think-toggle-icon" id="icon-${thinkId}"></i>
          </div>
          <div class="think-content" id="content-${thinkId}">${formatted}</div>
        </div>`;
      } else {
        return `<div class="main-content">${formatted}</div>`;
      }
    },

    /**
     * 🔥 在最终阶段基于 </think> 统一拆分思考过程和回答内容
     * 仅在服务端流结束 + 打字效果完成后调用，避免中途整块跳动
     */
    finalizeThinkingForMessage(msg) {
      if (!msg) return;
      // 已经处理过思考块则不再重复
      if (msg.thinkingLocked) return;

      const raw =
        msg.partialAnswer || msg.answerContent || msg.content || "";
      if (!raw || !raw.includes("</think>")) {
        return;
      }

      const parts = raw.split("</think>");
      let thinkContent = parts[0].replace(/^<think>/i, "").trim();
      const answerPart = parts
        .slice(1)
        .join("")
        .trim()
        .replace(/^\s*\n+/, "");

      if (thinkContent) {
        msg.thinkingContent = thinkContent;
        msg.thinkingLocked = true;
      }

      msg.answerContent = answerPart;
      msg.displayedAnswerContent = answerPart;

      // 同步更新兼容字段 content：先思考，再回答
      let fullContent = "";
      if (msg.thinkingContent) {
        fullContent += `<think>${msg.thinkingContent}</think>`;
      }
      if (msg.answerContent) {
        fullContent += msg.answerContent;
      }
      msg.content = fullContent;
    },

    /**
     * 🔥 在打字过程中，当“显示长度已经输出到 </think> 之前”时，
     * 立即将这部分内容移入思考框体，并让回答区域只保留 </think> 之后的内容。
     *
     * 注意：
     * - 这里只在流式阶段使用 thinkEndIndexRaw 进行拆分，保证思考内容是完整流式输出后才进入思考框体
     */
    maybeFinalizeThinkingDuringTyping(messageIndex) {
      const msg = this.messages[messageIndex];
      if (!msg) return;
      // 已经锁定思考块或尚未检测到 </think>，无需处理
      if (msg.thinkingLocked || msg.thinkEndIndexRaw == null) return;

      const displayedLen = (msg.displayedAnswerContent || "").length;
      // 只有当“已经显示到 </think> 之前”为止时才进行拆分
      if (displayedLen < msg.thinkEndIndexRaw) return;

      const raw =
        msg.partialAnswer || msg.answerContent || msg.content || "";
      if (!raw || raw.length <= msg.thinkEndIndexRaw) return;

      const beforeEnd = raw.slice(0, msg.thinkEndIndexRaw);
      const afterEnd = raw
        .slice(msg.thinkEndIndexRaw + "</think>".length)
        .trim()
        .replace(/^\s*\n+/, "");

      let thinkContent = beforeEnd.replace(/^<think>/i, "").trim();
      if (thinkContent) {
        msg.thinkingContent = thinkContent;
        msg.thinkingLocked = true;
      }

      msg.answerContent = afterEnd;
      // 重置回答区域的显示内容，让后续打字效果只对“真正的回答部分”生效
      msg.displayedAnswerContent = "";

      // 同步更新兼容字段 content：先思考，再回答
      let fullContent = "";
      if (msg.thinkingContent) {
        fullContent += `<think>${msg.thinkingContent}</think>`;
      }
      if (msg.answerContent) {
        fullContent += msg.answerContent;
      }
      msg.content = fullContent;
    },

    async startNewChat() {
      // 如果正在加载，先停止
      if (this.isLoading) {
        await this.stopGeneration();
      }

      this.currentConversationId = null;
      this.messages = [];
      this.attachments = [];
      this.currentTaskId = null;
      this.abortController = null;
      this.streamController = null;
      this.currentSuggestedQuestions = [];
      this.conversationInputs = {}; // 清空保存的变量

      // 重置变量值为默认值
      const resetValues = {};
      this.appConfig.variables.forEach((v) => {
        const varName = v.variable || v.name;
        if (v.type === "file" || v.type === "file-list") {
          resetValues[varName] = v.type === "file-list" ? [] : null;
        } else if (v.type === "select") {
          resetValues[varName] = v.default || (v.options && v.options[0]) || "";
        } else {
          resetValues[varName] = v.default || "";
        }
      });
      this.variableValues = resetValues;

      this.$message.success("已开启新对话");
    },

    async loadConversation(conversationId) {
      console.log(
        "🔄 loadConversation 被调用，conversationId:",
        conversationId,
        "isLoadingHistory:",
        this.isLoadingHistory
      );

      // 如果正在加载历史记录，直接返回
      if (this.isLoadingHistory) {
        console.log("⚠️ 正在加载历史记录，跳过此次请求");
        return;
      }

      // 如果正在生成消息，先停止
      if (this.isLoading) {
        await this.stopGeneration();
      }

      // 如果点击的是当前对话，不重复加载
      if (
        this.currentConversationId === conversationId &&
        this.messages.length > 0
      ) {
        console.log("⚠️ 已经是当前对话，跳过加载");
        return;
      }

      this.isLoadingHistory = true;
      this.currentConversationId = conversationId;
      this.messages = [];
      this.attachments = [];
      this.currentSuggestedQuestions = [];
      this.conversationInputs = {}; // 清空保存的变量（加载历史会话时无法恢复）

      // 显示加载提示
      const loading = this.$loading({
        lock: true,
        text: "加载对话历史中...",
        spinner: "el-icon-loading",
        background: "rgba(0, 0, 0, 0.7)",
      });

      try {
        // 获取历史消息，传递 App名称（如果存在）
        const result = await getMessages(
          conversationId,
          this.userId,
          50,
          this.appName
        );

        console.log("📥 API 返回的历史消息数量:", result?.data?.length);

        if (result && result.data && result.data.length > 0) {
          // API 返回的是倒序（最新的在前），需要反转为正序（最旧的在前）
          // 按 created_at 排序，从旧到新
          const historyData = [...result.data].sort(
            (a, b) => a.created_at - b.created_at
          );

          console.log("📊 开始处理 %d 条历史消息", historyData.length);

          // 将每条消息转换为用户消息和AI回复（messages 已经在上面清空过了）
          historyData.forEach((msg, index) => {
            // 1. 添加用户消息
            if (msg.query) {
              // 处理用户上传的文件（从 message_files 中提取属于用户的文件）
              let uploadedFilesInfo = [];
              if (msg.message_files && msg.message_files.length > 0) {
                console.log(
                  `      📎 检测到 ${msg.message_files.length} 个 message_files:`,
                  msg.message_files
                );

                // 过滤用户上传的文件
                // belongs_to 可能是 'user' 或 'assistant'，也可能不存在
                const userFiles = msg.message_files.filter((file) => {
                  // 尝试多种判断方式
                  const isUserFile =
                    file.belongs_to === "user" ||
                    file.created_by_role === "user" ||
                    (!file.belongs_to && !file.created_by_role); // 如果没有明确标记，也认为是用户的

                  console.log(`        文件 ${file.name || file.filename}:`, {
                    belongs_to: file.belongs_to,
                    created_by_role: file.created_by_role,
                    isUserFile: isUserFile,
                  });

                  return isUserFile;
                });

                uploadedFilesInfo = userFiles.map((file) => {
                  let fileUrl = file.url;

                  // 🔥 用户上传的文件也需要代理转换
                  if (this.isInternalUrl(fileUrl)) {
                    try {
                      const urlObj = new URL(fileUrl);
                      const filePath = urlObj.pathname + urlObj.search;

                      if (filePath.includes('/files/')) {
                        const filePathFromFiles = filePath.substring(filePath.indexOf('/files/') + '/files/'.length);
                        const baseApi = process.env.VUE_APP_BASE_API || '/api';
                        fileUrl = `${baseApi}/api/ai/chat/files/proxy/${filePathFromFiles}`;
                        console.log(`      🔄 [历史记录-用户文件] 内部地址转换为代理: ${file.url} -> ${fileUrl}`);
                      }
                    } catch (e) {
                      console.error(`      ❌ [历史记录-用户文件] URL 解析失败:`, e);
                    }
                  }

                  return {
                    name: file.name || file.filename || "文件",
                    size: file.size || 0,
                    url: fileUrl,
                  };
                });

                console.log(
                  `      ✅ 提取到 ${uploadedFilesInfo.length} 个用户文件`
                );
              }

              const userMsg = {
                role: "user",
                content: msg.query,
                created_at: msg.created_at * 1000,
                messageId: `${msg.id}-user`, // 添加后缀区分用户消息
              };

              // 如果有文件信息，添加到消息对象中
              if (uploadedFilesInfo.length > 0) {
                userMsg.uploadedFiles = uploadedFilesInfo;
                console.log(
                  `      ✅✅✅ 用户消息包含 ${uploadedFilesInfo.length} 个文件:`,
                  uploadedFilesInfo
                );
              }

              this.messages.push(userMsg);
              console.log(
                `  [${index + 1}] ➡️ 用户: ${msg.query.substring(0, 30)}...`,
                uploadedFilesInfo.length > 0
                  ? `(${uploadedFilesInfo.length}个文件)`
                  : ""
              );
              console.log(
                `      完整消息对象:`,
                JSON.stringify(userMsg, null, 2)
              );
            }

            // 2. 构建 AI 回复内容
            let assistantContent = "";

            const answerHasThinkTags =
              msg.answer && msg.answer.includes("<think>");
            const hasAgentThoughts =
              Array.isArray(msg.agent_thoughts) &&
              msg.agent_thoughts.length > 0;

            // 【核心逻辑优化】
            // 1. 优先使用 Dify API 返回的结构化数据（agent_thoughts）
            // 2. 只在 answer 中没有 <think> 标签时，才从 agent_thoughts 构建思考块
            // 3. 不再依赖固定关键词判断，完全基于 API 返回的数据结构

            if (!answerHasThinkTags && hasAgentThoughts) {
              console.log(
                `      ✅ 从 agent_thoughts 构建 ${msg.agent_thoughts.length} 个思考块`
              );
              msg.agent_thoughts.forEach((thought, idx) => {
                if (thought.thought) {
                  let thinkContent = `**Agent 思考 ${
                    idx + 1
                  }:**\n${thought.thought.trim()}`;

                  if (thought.tool) {
                    thinkContent += `\n\n**工具:** ${thought.tool}`;
                  }
                  if (thought.observation) {
                    thinkContent += `\n\n**观察结果:** ${thought.observation}`;
                  }

                  // 统一使用 <think> 标签包裹思考内容
                  assistantContent += `<think>${thinkContent}</think>`;
                }
              });
            } else if (answerHasThinkTags) {
              console.log(
                `      ℹ️ answer 中已包含 <think> 标签，使用开发者自定义的思考内容`
              );
            }

            // 2.3 添加 answer 内容
            if (msg.answer && msg.answer.trim()) {
              // 如果没有 agent_thoughts 且没有 <think> 标签，尝试智能检测
              if (
                !answerHasThinkTags &&
                !hasAgentThoughts &&
                this.detectThinkingContent(msg.answer)
              ) {
                const { thinkingPart, answerPart } =
                  this.extractThinkingContent(msg.answer);
                if (thinkingPart) {
                  assistantContent += `<think>${thinkingPart}</think>`;
                  console.log("      ✅ 智能识别到历史消息中的思考过程");
                  if (answerPart) {
                    assistantContent += answerPart;
                  }
                } else {
                  assistantContent += msg.answer;
                }
              } else {
                assistantContent += msg.answer;
              }
              console.log(
                `      ⬅️ AI 回答: ${msg.answer.substring(0, 30)}... (${
                  msg.answer.length
                }字)`
              );
            } else {
              console.log(`      ⚠️ 没有 answer 内容`);
            }

            // 2.3 如果有消息文件（只处理 AI 生成的文件，用户上传的文件已在用户消息中显示）
            if (msg.message_files && msg.message_files.length > 0) {
              // 只显示 AI 生成的文件（assistant 的文件）
              const assistantFiles = msg.message_files.filter(
                (file) =>
                  file.belongs_to === "assistant" ||
                  file.created_by_role === "assistant"
              );

              if (assistantFiles.length > 0) {
                assistantContent += "\n\n";
                assistantFiles.forEach((file, fileIdx) => {
                  console.log(
                    `      📎 AI 文件 ${fileIdx + 1}:`,
                    JSON.stringify(file, null, 2)
                  );

                  // 🔥 统一处理文件URL代理
                  let fileUrl = file.url;
                  if (this.isInternalUrl(fileUrl)) {
                    try {
                      const urlObj = new URL(fileUrl);
                      const filePath = urlObj.pathname + urlObj.search;

                      if (filePath.includes('/files/')) {
                        const filePathFromFiles = filePath.substring(filePath.indexOf('/files/') + '/files/'.length);
                        const baseApi = process.env.VUE_APP_BASE_API || '/api';
                        fileUrl = `${baseApi}/api/ai/chat/files/proxy/${filePathFromFiles}`;

                        console.log(`      🔄 [历史记录-${file.type}] 内部地址转换为代理: ${file.url} -> ${fileUrl}`);
                      }
                    } catch (e) {
                      console.error(`      ❌ [历史记录-${file.type}] URL 解析失败:`, e);
                    }
                  }

                  if (file.type === "image") {
                    assistantContent += `\n<img src="${fileUrl}" alt="生成的图片" style="max-width: 100%; border-radius: 8px; margin-top: 8px;" />`;
                  } else {
                    // 尝试多种方式获取文件名
                    let fileName = file.name || file.filename || file.title;

                    // 尝试从 belongs_to 字段获取
                    if (!fileName && file.belongs_to) {
                      fileName =
                        file.belongs_to.name || file.belongs_to.filename;
                    }

                    // 尝试从 extension_data 获取
                    if (!fileName && file.extension_data) {
                      fileName =
                        file.extension_data.name ||
                        file.extension_data.filename;
                    }

                    // 尝试从 URL 查询参数中获取（Dify 可能在 URL 中包含文件名）
                    if (!fileName && file.url) {
                      try {
                        const urlObj = new URL(file.url);
                        // 检查查询参数
                        const nameParam =
                          urlObj.searchParams.get("name") ||
                          urlObj.searchParams.get("filename") ||
                          urlObj.searchParams.get("file_name");
                        if (nameParam) {
                          fileName = decodeURIComponent(nameParam);
                        } else {
                          // 尝试从路径提取
                          const pathParts = urlObj.pathname.split("/");
                          const lastPart = pathParts[pathParts.length - 1];
                          if (lastPart && lastPart !== "file-preview") {
                            fileName = decodeURIComponent(lastPart);
                          }
                        }
                      } catch (e) {
                        console.warn("无法从 URL 提取文件名:", e);
                      }
                    }

                    // 最后的回退选项
                    if (!fileName || fileName === "file-preview") {
                      fileName = `${file.type || "document"}文件`;
                    }

                    console.log(`      📎 最终使用的文件名: ${fileName}`);

                    // 🔥 处理文件URL代理（与流式响应中的逻辑一致）
                    let fileUrl = file.url;

                    // 检测是否需要使用后端代理
                    if (this.isInternalUrl(fileUrl)) {
                      try {
                        const urlObj = new URL(fileUrl);
                        const filePath = urlObj.pathname + urlObj.search; // 包含路径和查询参数

                        // 从 /files/ 开始提取路径
                        if (filePath.includes('/files/')) {
                          const filePathFromFiles = filePath.substring(filePath.indexOf('/files/') + '/files/'.length);

                          // 转换为代理URL
                          const baseApi = process.env.VUE_APP_BASE_API || '/api';
                          fileUrl = `${baseApi}/api/ai/chat/files/proxy/${filePathFromFiles}`;

                          console.log(`      🔄 [历史记录] 内部地址转换为代理: ${file.url} -> ${fileUrl}`);
                        } else {
                          console.warn(`      ⚠️ [历史记录] 无法提取文件路径: ${fileUrl}`);
                        }
                      } catch (e) {
                        console.error(`      ❌ [历史记录] URL 解析失败:`, e);
                      }
                    } else {
                      console.log(`      🌐 [历史记录] 公网地址，直接使用: ${fileUrl}`);
                    }

                    // 生成文件卡片HTML（与流式响应一致）
                    const fileExt = fileName.split('.').pop().toLowerCase();
                    const fileSize = file.size ? this.formatFileSize(file.size) : '';
                    const fileIcon = this.getFileIcon(fileExt);
                    const cardId = `file-card-history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

                    assistantContent += `\n<div class="file-card" data-card-id="${cardId}"><div class="file-icon"><i class="el-icon-${fileIcon}"></i><span class="file-ext">.${fileExt}</span></div><div class="file-info"><div class="file-name" title="${fileName}">${fileName}</div>${fileSize ? `<div class="file-size">${fileSize}</div>` : ''}</div><a href="${fileUrl}" download="${fileName}" class="file-download" title="下载文件"><i class="el-icon-download"></i></a></div>`;
                  }
                });
              }
            }

            // 3. 添加 AI 回复消息（只有当有内容时才添加）
            if (assistantContent.trim()) {
              // 🔥 使用新的分离结构，避免历史记录闪烁
              const hasEndTag = assistantContent.includes("</think>");
              let thinkingContent = "";
              let answerContent = "";
              let thinkingLocked = false;

              if (hasEndTag) {
                // 分离思考和回答
                const parts = assistantContent.split("</think>");
                thinkingContent = parts[0].replace(/^<think>/i, "").trim();
                answerContent = parts
                  .slice(1)
                  .join("")
                  .trim()
                  .replace(/^\s*\n+/, "");
                thinkingLocked = true;
              } else {
                // 没有思考过程，全是回答
                answerContent = assistantContent;
              }

              this.messages.push({
                role: "assistant",
                content: assistantContent, // 保留完整内容用于兼容
                thinkingContent: thinkingContent,
                answerContent: answerContent,
                displayedAnswerContent: answerContent, // 🔥 历史消息直接显示完整内容，不需要打字效果
                thinkingLocked: thinkingLocked,
                typingTimer: null, // 🔥 打字效果定时器
                partialAnswer: assistantContent, // 历史消息视为已完整接收
                isStreamFinished: true, // 🔥 历史记录默认已结束，直接使用完整表格/HTML 渲染
                serverStreamDone: true,
                created_at: msg.created_at * 1000 + 1, // 稍微偏移时间戳，避免与用户消息完全相同
                messageId: `${msg.id}-assistant`, // 添加后缀区分 AI 消息
                hasAgentThoughts: Boolean(msg.agent_thoughts),
                hasFiles: msg.message_files && msg.message_files.length > 0,
              });
            }
          });

          console.log(
            "✅ 历史记录加载完成，共 %d 条消息（%d 轮对话）",
            this.messages.length,
            historyData.length
          );

          // 🔥 加载历史记录后强制滚动到底部，默认展开思考过程
          this.$nextTick(() => {
            this.forceScrollToBottom();
            // 🔥 默认展开思考过程，不再自动折叠
            // this.autoCollapseThinkBlocks()
          });

          this.$message.success(`已加载 ${historyData.length} 轮对话`);
        } else {
          this.$message.info("该对话暂无历史记录");
        }
      } catch (error) {
        console.error("加载对话历史失败:", error);
        this.$message.error(
          "加载对话历史失败：" + (error.message || "未知错误")
        );
      } finally {
        loading.close();
        this.isLoadingHistory = false; // 重置加载状态
      }
    },

    async loadConversationsList() {
      try {
        // 传递 App名称（如果存在）
        const result = await getConversations(this.userId, 50, this.appName);
        this.conversations = result.data || [];
      } catch (error) {
        console.error("加载会话列表失败:", error);
      }
    },

    async deleteChat(conversationId) {
      try {
        await this.$confirm("确定要删除这条对话记录吗？", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        });

        await deleteConversation(conversationId, this.userId, this.appName);

        if (this.currentConversationId === conversationId) {
          this.startNewChat();
        }

        await this.loadConversationsList();
        this.$message.success("已删除");
      } catch (error) {
        if (error !== "cancel") {
          console.error("删除会话失败:", error);
          const errorMessage = error.message || "未知错误";
          this.$message.error("删除失败: " + errorMessage);
        }
      }
    },

    // Chatflow 文件上传
    triggerChatflowFileUpload() {
      if (this.$refs.chatflowFile) {
        this.$refs.chatflowFile.click();
      }
    },

    handleChatflowFileUpload(e) {
      const files = Array.from((e && e.target && e.target.files) || []);
      if (!files.length) return;

      // 仅保留 Dify 文档提取器支持的文件类型，其余给出提示
      const validFiles = [];
      const invalidFiles = [];

      files.forEach((file) => {
        if (this.isDifySupportedDocFile(file.name)) {
          validFiles.push(file);
        } else {
          invalidFiles.push(file.name);
        }
      });

      if (invalidFiles.length > 0) {
        // 不支持的文件用红色标签样式展示
        const invalidTags = invalidFiles
          .map(
            (name) =>
              `<span style="display:inline-block;padding:2px 10px;margin:3px 6px 3px 0;background:#fef0f0;color:#f56c6c;border-radius:4px;font-size:13px;">${name}</span>`
          )
          .join("");

        // 支持的文件类型用蓝色标签样式展示
        const supportTags = DIFY_SUPPORTED_DOC_EXTS.map(
          (ext) =>
            `<span style="display:inline-block;padding:2px 10px;margin:3px 6px 3px 0;background:#ecf5ff;color:#409EFF;border-radius:4px;font-size:13px;">.${ext}</span>`
        ).join("");

        // 使用弹窗而不是右下角消息，让提示更加明显
        this.$alert(
          `<div style="margin-bottom:16px;">` +
            `<div style="color:#606266;margin-bottom:8px;">以下文件类型不受文档提取器支持，请重新上传：</div>` +
            `<div style="display:flex;flex-wrap:wrap;">${invalidTags}</div>` +
            `</div>` +
            `<div>` +
            `<div style="color:#606266;margin-bottom:8px;">当前支持的文件类型：</div>` +
            `<div style="display:flex;flex-wrap:wrap;max-height:180px;overflow-y:auto;padding:4px 0;">${supportTags}</div>` +
            `</div>`,
          "文件类型不支持",
          {
            type: "warning",
            dangerouslyUseHTMLString: true,
            confirmButtonText: "我知道了",
            customClass: "dify-file-type-alert",
          }
        );
      }

      if (!validFiles.length) {
        if (this.$refs.chatflowFile) {
          this.$refs.chatflowFile.value = "";
        }
        return;
      }

      const mapped = validFiles.map((f) => ({
        uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
        name: f.name,
        size: f.size,
        raw: f,
      }));

      this.attachments = this.attachments.concat(mapped);
      this.$message.success(`已选择 ${mapped.length} 个可用文件`);

      if (this.$refs.chatflowFile) {
        this.$refs.chatflowFile.value = "";
      }
    },

    // 通用文件上传
    triggerGeneralFileUpload() {
      if (this.$refs.generalFileUpload) {
        this.$refs.generalFileUpload.click();
      }
    },

    handleGeneralFileUpload(e) {
      const files = Array.from((e && e.target && e.target.files) || []);
      if (!files.length) return;

      // 允许的文件类型
      const allowedTypes = [
        "doc",
        "docx",
        "xls",
        "xlsx",
        "pdf",
        "jpg",
        "jpeg",
        "bmp",
        "txt",
        "md",
        "html",
      ];

      // 验证文件类型
      const validFiles = [];
      const invalidFiles = [];

      files.forEach((file) => {
        const ext = (file.name.split(".").pop() || "").toLowerCase();
        if (allowedTypes.includes(ext)) {
          validFiles.push(file);
        } else {
          invalidFiles.push(file.name);
        }
      });

      // 如果有不支持的文件类型，提示用户
      if (invalidFiles.length > 0) {
        this.$message.warning(
          `以下文件类型不支持：${invalidFiles.join(
            "、"
          )}。支持的类型：doc、docx、xls、xlsx、pdf、jpg、bmp、txt、md、html`
        );
      }

      // 如果没有有效文件，直接返回
      if (validFiles.length === 0) {
        if (this.$refs.generalFileUpload) {
          this.$refs.generalFileUpload.value = "";
        }
        return;
      }

      // 检查文件大小（限制为10MB）
      const maxSize = 10 * 1024 * 1024; // 10MB
      const oversizedFiles = [];
      const sizeValidFiles = [];

      validFiles.forEach((file) => {
        if (file.size > maxSize) {
          oversizedFiles.push(file.name);
        } else {
          sizeValidFiles.push(file);
        }
      });

      if (oversizedFiles.length > 0) {
        this.$message.warning(
          `以下文件超过10MB限制：${oversizedFiles.join("、")}`
        );
      }

      if (sizeValidFiles.length === 0) {
        if (this.$refs.generalFileUpload) {
          this.$refs.generalFileUpload.value = "";
        }
        return;
      }

      // 将有效文件添加到附件列表
      const mapped = sizeValidFiles.map((f) => ({
        uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
        name: f.name,
        size: f.size,
        raw: f,
      }));

      this.attachments = this.attachments.concat(mapped);
      this.$message.success(`已选择 ${sizeValidFiles.length} 个文件`);

      if (this.$refs.generalFileUpload) {
        this.$refs.generalFileUpload.value = "";
      }
    },

    removeAttachment(index) {
      if (index < 0 || index >= this.attachments.length) return;
      const removed = this.attachments.splice(index, 1)[0];
      if (removed) {
        this.$message.success("已移除：" + (removed.name || "文件"));
      }
    },

    /**
     * 处理变量文件上传
     */
    handleVariableFileUpload(event, varName, isMultiple) {
      const files = Array.from((event && event.target && event.target.files) || []);
      if (!files.length) return;

      if (isMultiple) {
        // 文件列表
        const mapped = files.map((f) => ({
          uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
          name: f.name,
          size: f.size,
          raw: f,
        }));
        this.$set(this.variableValues, varName, mapped);
        this.$message.success(`已选择 ${files.length} 个文件`);
      } else {
        // 单个文件
        const file = files[0];
        this.$set(this.variableValues, varName, {
          uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
          name: file.name,
          size: file.size,
          raw: file,
        });
        this.$message.success(`已选择文件：${file.name}`);
      }

      // 清空文件输入
      if (event.target) {
        event.target.value = "";
      }
    },

    /**
     * 移除变量文件
     */
    removeVariableFile(varName, index) {
      const files = this.variableValues[varName];
      if (Array.isArray(files) && index >= 0 && index < files.length) {
        files.splice(index, 1);
        this.$message.success("已移除文件");
      }
    },

    getFileType(filename) {
      const ext = (filename.split(".").pop() || "").toLowerCase();
      const imageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg"];
      const docExts = [
        "txt",
        "md",
        "pdf",
        "doc",
        "docx",
        "html",
        "xlsx",
        "xls",
        "csv",
      ];

      if (imageExts.includes(ext)) return "image";
      if (docExts.includes(ext)) return "document";
      return "custom";
    },

    /**
     * 从上传结果中提取 Dify 文件 ID
     * @param {Object} uploadResult - 上传结果，格式为 { code: 200, data: AiFile }
     * @returns {string|null} Dify 文件 ID
     */
    extractDifyFileId(uploadResult) {
      try {
        // uploadResult 是 ResponseResult 格式：{ code: 200, data: AiFile }
        const fileData = uploadResult?.data || uploadResult;
        if (DEBUG_STREAM_LOG) {
          console.log("🔍 [调试] 完整的上传结果:", uploadResult);
          console.log("🔍 [调试] 文件数据:", fileData);
        }

        // 从 difyResponse 中解析 Dify 文件 ID
        if (fileData?.difyResponse) {
          if (DEBUG_STREAM_LOG) {
            console.log("🔍 [调试] 原始 difyResponse:", fileData.difyResponse);
          }
          let difyResponse = null;
          if (typeof fileData.difyResponse === "string") {
            difyResponse = JSON.parse(fileData.difyResponse);
          } else {
            difyResponse = fileData.difyResponse;
          }

          if (DEBUG_STREAM_LOG) {
            console.log("🔍 [调试] 解析后的 difyResponse:", difyResponse);
          }

          // 尝试从多个可能的字段中获取文件 ID
          const fileId =
            difyResponse?.id ||
            difyResponse?.file_id ||
            difyResponse?.upload_file_id;
          if (fileId) {
            if (DEBUG_STREAM_LOG) {
              console.log("✅ [调试] 找到 Dify 文件 ID:", fileId);
            }
            return fileId;
          }
        } else if (DEBUG_STREAM_LOG) {
          console.warn("⚠️ [调试] 没有 difyResponse 字段");
        }

        // 如果没有 difyResponse，尝试使用数据库 ID（不推荐，但作为后备）
        if (DEBUG_STREAM_LOG) {
          console.warn(
            "未找到 Dify 文件 ID，使用数据库 ID 作为后备:",
            fileData?.id
          );
        }
        return fileData?.id || null;
      } catch (error) {
        console.error("提取 Dify 文件 ID 失败:", error);
        return null;
      }
    },

    formatSize(bytes) {
      if (bytes < 1024) return bytes + "B";
      const kb = bytes / 1024;
      if (kb < 1024) return kb.toFixed(2) + "KB";
      const mb = kb / 1024;
      return mb.toFixed(2) + "MB";
    },

    formatTime(timestamp) {
      const date = new Date(timestamp);
      const now = new Date();
      const diff = now - date;

      if (diff < 60000) return "刚刚";
      if (diff < 3600000) return Math.floor(diff / 60000) + "分钟前";
      if (diff < 86400000) return Math.floor(diff / 3600000) + "小时前";

      return date.toLocaleString("zh-CN", {
        month: "2-digit",
        day: "2-digit",
        hour: "2-digit",
        minute: "2-digit",
      });
    },

    /**
     * 🔥 处理 chat-body 滚动事件
     * 检测用户是否手动向上滚动，如果是则停止自动滚动到底部
     */
    handleChatBodyScroll() {
      if (!this.$refs.chatBody) return;

      const container = this.$refs.chatBody;
      const scrollTop = container.scrollTop;
      const scrollHeight = container.scrollHeight;
      const clientHeight = container.clientHeight;

      // 计算距离底部的距离（像素）
      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

      // 🔥 检测滚动方向：当前位置 vs 上一次位置
      const isScrollingUp = scrollTop < this.lastScrollTop;

      // 更新上一次滚动位置
      this.lastScrollTop = scrollTop;

      // 🔥 核心逻辑：
      // 1. 如果用户正在向上滚动，立即标记为已滚动（无论当前位置）
      // 2. 只有当用户主动滚动到底部附近（距离<50px）时，才重置标记
      if (isScrollingUp) {
        // 用户向上滚动，禁止自动滚动
        this.userHasScrolledUp = true;
      } else if (distanceFromBottom < 50) {
        // 用户滚动到接近底部，允许自动滚动
        this.userHasScrolledUp = false;
      }
    },

    /**
     * 🔥 处理鼠标滚轮事件
     * 在滚动发生前检测用户的滚动意图
     */
    handleChatBodyWheel(event) {
      // deltaY < 0 表示向上滚动，deltaY > 0 表示向下滚动
      if (event.deltaY < 0) {
        // 用户使用滚轮向上滚动，立即标记
        this.userHasScrolledUp = true;
      }
      // 向下滚动时不在这里重置标记，由 scroll 事件根据位置判断
    },

    /**
     * 🔥 滚动到底部（智能判断）
     * 只有在用户没有手动向上滚动时才自动滚动到底部
     * @param {boolean} force - 是否强制滚动（忽略用户滚动状态）
     */
    scrollToBottom(force = false) {
      // 使用外层 chat-body 容器进行滚动
      if (this.$refs.chatBody) {
        // 如果用户已手动向上滚动且不是强制滚动，则不自动滚动
        if (this.userHasScrolledUp && !force) {
          return;
        }
        this.$refs.chatBody.scrollTop =
          this.$refs.chatBody.scrollHeight;
      }
    },

    /**
     * 🔥 强制滚动到底部（用于用户发送消息等场景）
     */
    forceScrollToBottom() {
      this.userHasScrolledUp = false;
      this.scrollToBottom(true);
    },

    autoCollapseThinkBlocks() {
      // 自动折叠所有思考过程
      this.$nextTick(() => {
        const expandedThinkBlocks = document.querySelectorAll(
          ".think-content:not(.collapsed)"
        );

        if (expandedThinkBlocks.length > 0) {
          if (DEBUG_STREAM_LOG) {
            console.log(`🔽 折叠 ${expandedThinkBlocks.length} 个思考块`);
          }
          expandedThinkBlocks.forEach((block) => {
            const id = block.id.replace("content-", "");
            if (id) {
              window.toggleThinkBlock(id);
            }
          });
        }
      });
    },

    autoResizeTextarea() {
      if (this.$refs.textarea) {
        this.$refs.textarea.style.height = "auto";
        this.$refs.textarea.style.height =
          this.$refs.textarea.scrollHeight + "px";
      }
    },

    setQuickMessage(msg) {
      this.message = msg;
      this.$nextTick(() => {
        if (this.$refs.textarea) {
          this.$refs.textarea.focus();
        }
      });
    },

    async rethinkMessage(messageIndex) {
      // 找到这个 AI 回答对应的用户问题
      const aiMessage = this.messages[messageIndex];
      if (aiMessage.role !== "assistant") {
        this.$message.error("只能重新思考 AI 的回答");
        return;
      }

      // 向前查找最近的用户消息
      let userMessage = null;
      for (let i = messageIndex - 1; i >= 0; i--) {
        if (this.messages[i].role === "user") {
          userMessage = this.messages[i];
          break;
        }
      }

      if (!userMessage) {
        this.$message.error("未找到对应的用户问题");
        return;
      }

      // 确认操作
      try {
        await this.$confirm(
          "确定要重新思考这个问题吗？将删除当前回答并重新生成。",
          "提示",
          {
            confirmButtonText: "确定",
            cancelButtonText: "取消",
            type: "warning",
          }
        );
      } catch {
        return;
      }

      // 删除 AI 回答
      this.messages.splice(messageIndex, 1);

      // 重新发送用户问题
      await this.handleSend({
        fromRethink: true,
        reuseContent: userMessage.content,
      });
    },

    copyMessage(msg) {
      // 🔥 支持新的分离结构
      let content = "";

      // 如果是新结构，使用 answerContent（不包含思考）
      if (msg.answerContent) {
        content = msg.answerContent;
      } else if (msg.content) {
        // 兼容旧结构
        content = msg.content;
      } else {
        content = "";
      }

      // 移除 HTML 标签和 <think> 标签，只复制纯文本
      let textContent = content
        // 移除 <think> 标签及其内容
        .replace(/<think>[\s\S]*?<\/think>/gi, "")
        // 移除 HTML 标签
        .replace(/<[^>]+>/g, "")
        // 将 HTML 实体转回普通字符
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&amp;/g, "&")
        // 将 <br> 转为换行
        .replace(/<br\s*\/?>/gi, "\n")
        // 移除多余的空白
        .trim();

      // 复制到剪贴板
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard
          .writeText(textContent)
          .then(() => {
            this.$message.success("已复制到剪贴板");
          })
          .catch(() => {
            // 降级方案
            this.fallbackCopy(textContent);
          });
      } else {
        // 降级方案
        this.fallbackCopy(textContent);
      }
    },

    fallbackCopy(text) {
      const textarea = document.createElement("textarea");
      textarea.value = text;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      try {
        document.execCommand("copy");
        this.$message.success("已复制到剪贴板");
      } catch (err) {
        this.$message.error("复制失败，请手动复制");
      }
      document.body.removeChild(textarea);
    },

    resendMessage(content) {
      if (this.isLoading) {
        this.$message.warning("请等待当前消息完成");
        return;
      }

      // 直接设置到输入框
      this.message = content;
      this.$nextTick(() => {
        if (this.$refs.textarea) {
          this.$refs.textarea.focus();
        }
      });
    },

    /**
     * 处理变量文件选择（单文件变量）
     */
    handleVariableFileSelect(variable, e) {
      const files = Array.from((e && e.target && e.target.files) || []);
      if (!files.length) return;

      const varName = variable.variable || variable.name;
      const file = files[0];

      // 检查 .doc 文件
      if (
        file.name.toLowerCase().endsWith(".doc") &&
        !file.name.toLowerCase().endsWith(".docx")
      ) {
        this.$message.error("不支持 .doc 格式文件，请使用 .docx 格式");
        if (e.target) {
          e.target.value = "";
        }
        return;
      }

      this.$set(this.variableValues, varName, {
        uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
        name: file.name,
        size: file.size,
        raw: file,
      });

      this.$message.success(`已选择文件：${file.name}`);
      if (e.target) {
        e.target.value = "";
      }
    },

    /**
     * 处理变量文件列表选择（文件列表变量）
     */
    handleVariableFileListSelect(variable, e) {
      const files = Array.from((e && e.target && e.target.files) || []);
      if (!files.length) return;

      const varName = variable.variable || variable.name;

      // 检查 .doc 文件
      const docFiles = files.filter((f) => {
        const name = f.name.toLowerCase();
        return name.endsWith(".doc") && !name.endsWith(".docx");
      });
      if (docFiles.length > 0) {
        this.$message.error(
          "不支持 .doc 格式文件，请使用 .docx 格式。不支持的文件：" +
          docFiles.map((f) => f.name).join("、")
        );
        if (e.target) {
          e.target.value = "";
        }
        return;
      }

      const mapped = files.map((f) => ({
        uid: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
        name: f.name,
        size: f.size,
        raw: f,
      }));

      // 添加到现有列表
      if (!this.variableValues[varName]) {
        this.$set(this.variableValues, varName, []);
      }
      this.$set(
        this.variableValues,
        varName,
        this.variableValues[varName].concat(mapped)
      );

      this.$message.success(`已添加 ${files.length} 个文件`);
      if (e.target) {
        e.target.value = "";
      }
    },

    /**
     * 移除变量文件（单文件变量）
     */
    removeVariableFile(variable) {
      const varName = variable.variable || variable.name;
      this.$set(this.variableValues, varName, null);
      this.$message.success("已移除文件");
    },

    /**
     * 移除变量文件列表中的某个文件
     */
    removeVariableFileFromList(variable, index) {
      const varName = variable.variable || variable.name;
      if (
        this.variableValues[varName] &&
        Array.isArray(this.variableValues[varName])
      ) {
        const removed = this.variableValues[varName].splice(index, 1)[0];
        if (removed) {
          this.$message.success(`已移除：${removed.name}`);
        }
      }
    },

    /**
     * 智能检测 answer 中是否包含思考过程
     * @param {string} content - 要检测的内容
     * @returns {boolean} 是否包含思考过程
     */
    detectThinkingContent(content) {
      if (!content || typeof content !== "string") return false;

      // 🔥 优化的思考过程检测策略

      // 1. 检测典型开头（思考过程通常以这些词开头）
      const thinkingStarters = [
        /^嗯[，,、。\s]/,
        /^好的[，,、。\s]/,
        /^首先[，,、。\s]/,
        /^让我[想想|思考|分析|看看]/,
        /^我[需要|将|应该|得|要]/,
        /^用户[问|询问|提出|希望|需要]/,
        /^现在[需要|我]/,
        /^接下来[，,、。\s]/,
        /^Okay|Alright|Let me|I need to|First/i,
      ];

      const hasThinkingStarter = thinkingStarters.some((pattern) =>
        pattern.test(content.trim())
      );

      // 2. 检测思考过程的特征词汇（出现频率）
      const thinkingKeywords = [
        "需要",
        "应该",
        "可能",
        "或许",
        "首先",
        "然后",
        "接下来",
        "最后",
        "分析",
        "思考",
        "考虑",
        "注意",
        "检查",
        "确认",
        "验证",
        "提取",
        "整理",
        "理解",
        "识别",
        "判断",
        "评估",
        "处理",
        "解决",
        "用户",
        "问题",
        "内容",
        "信息",
        "数据",
        "结果",
      ];

      const keywordCount = thinkingKeywords.filter((keyword) =>
        content.includes(keyword)
      ).length;

      // 3. 检测是否包含过多的推理性语句
      const reasoningPatterns = [
        /因为|所以|如果|那么|但是|不过|然而|可能|或许/g,
        /需要|应该|必须|可以|能够|要求/g,
        /首先|其次|然后|接下来|最后|总之/g,
      ];

      let reasoningCount = 0;
      reasoningPatterns.forEach((pattern) => {
        const matches = content.match(pattern);
        if (matches) reasoningCount += matches.length;
      });

      // 4. 检测内容结构（思考过程通常较长且分段）
      const paragraphs = content
        .split(/\n\n+/)
        .filter((p) => p.trim().length > 0);
      const hasMultipleParagraphs = paragraphs.length >= 3;
      const isLongContent = content.length > 200;

      // 5. 排除明显的正式回答（包含标题、列表等结构化内容）
      const hasFormalStructure = /^#+\s|^\*\*[^*]+\*\*\s*$|^---$|^===$/m.test(
        content
      );

      // 综合判断逻辑：
      // - 有思考性开头 + (多个关键词 或 多个推理词 或 多段落)
      // - 不包含正式的结构化格式
      const isThinking =
        ((hasThinkingStarter &&
            (keywordCount >= 3 ||
              reasoningCount >= 5 ||
              hasMultipleParagraphs)) ||
          (keywordCount >= 5 && reasoningCount >= 8 && isLongContent)) &&
        !hasFormalStructure;

      return isThinking;
    },

    /**
     * 从内容中提取思考过程和回答部分
     * @param {string} content - 原始内容
     * @returns {Object} { thinkingPart, answerPart }
     */
    extractThinkingContent(content) {
      if (!content) return { thinkingPart: null, answerPart: content };

      // 尝试多种分离策略

      // 策略1：基于标题分离（如 # 执法检查通知书）
      const titleMatch = content.match(/^(.*?)(#\s*[^#\n]+.*)$/s);
      if (titleMatch) {
        const thinkingPart = titleMatch[1].trim();
        const answerPart = titleMatch[2].trim();

        if (
          thinkingPart.length > 50 &&
          this.detectThinkingContent(thinkingPart)
        ) {
          return { thinkingPart, answerPart };
        }
      }

      // 策略2：基于分隔符分离（如 --- 或 ===）
      const separatorMatch = content.match(/^(.*?)(?:---|===|___)(.*)$/s);
      if (separatorMatch) {
        const thinkingPart = separatorMatch[1].trim();
        const answerPart = separatorMatch[2].trim();

        if (
          thinkingPart.length > 50 &&
          this.detectThinkingContent(thinkingPart)
        ) {
          return { thinkingPart, answerPart };
        }
      }

      // 策略3：基于内容特征分离
      // 查找第一个明显的"结果"或"输出"部分
      const resultPatterns = [
        /(.*?)(#\s*[^#\n]+.*)$/s, // 标题开始
        /(.*?)(##\s*[^#\n]+.*)$/s, // 二级标题开始
        /(.*?)(\*\*[^*]+\*\*.*)$/s, // 粗体开始
        /(.*?)(一、[^一]*二、.*)$/s, // 中文序号开始
      ];

      for (const pattern of resultPatterns) {
        const match = content.match(pattern);
        if (match) {
          const thinkingPart = match[1].trim();
          const answerPart = match[2].trim();

          if (
            thinkingPart.length > 50 &&
            this.detectThinkingContent(thinkingPart)
          ) {
            return { thinkingPart, answerPart };
          }
        }
      }

      // 策略4：基于长度和内容特征智能分离
      const lines = content.split("\n");
      let thinkingLines = [];
      let answerLines = [];
      let foundAnswerStart = false;

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];

        // 检测是否是答案开始的标志
        if (
          !foundAnswerStart &&
          (line.match(/^#\s/) || // 标题
            line.match(/^##\s/) || // 二级标题
            line.match(/^\*\*[^*]+\*\*$/) || // 粗体标题
            line.match(/^[一二三四五六七八九十]+、/) || // 中文序号
            line.match(/^[1-9]+\./)) // 数字序号
        ) {
          foundAnswerStart = true;
          answerLines.push(line);
        } else if (foundAnswerStart) {
          answerLines.push(line);
        } else {
          thinkingLines.push(line);
        }
      }

      const thinkingPart = thinkingLines.join("\n").trim();
      const answerPart = answerLines.join("\n").trim();

      if (
        thinkingPart.length > 50 &&
        answerPart.length > 20 &&
        this.detectThinkingContent(thinkingPart)
      ) {
        return { thinkingPart, answerPart };
      }

      // 如果无法分离，返回原内容
      return { thinkingPart: null, answerPart: content };
    },

    /**
     * 判断是否为代码类型的消息
     */
    isCodeMessage(content) {
      if (!content) return false;

      // 检测是否主要是代码内容
      const codeBlockPattern = /```[\s\S]*?```/g;
      const matches = content.match(codeBlockPattern);

      if (!matches) return false;

      // 计算代码块内容占比
      const codeLength = matches.reduce((sum, match) => sum + match.length, 0);
      const totalLength = content.length;

      // 如果代码块占比超过60%，认为是代码消息
      return codeLength / totalLength > 0.6;
    },

    /**
     * 检测代码语言
     */
    detectCodeLanguage(content) {
      if (!content) return "plaintext";

      const match = content.match(/```(\w+)/);
      if (match && match[1]) {
        const lang = match[1].toLowerCase();

        // 语言映射
        const langMap = {
          js: "JavaScript",
          javascript: "JavaScript",
          ts: "TypeScript",
          typescript: "TypeScript",
          py: "Python",
          python: "Python",
          java: "Java",
          sql: "SQL",
          html: "HTML",
          css: "CSS",
          scss: "SCSS",
          json: "JSON",
          xml: "XML",
          bash: "Bash",
          shell: "Shell",
          sh: "Shell",
          vue: "Vue",
          jsx: "JSX",
          tsx: "TSX",
          go: "Go",
          rust: "Rust",
          c: "C",
          cpp: "C++",
          csharp: "C#",
          php: "PHP",
          ruby: "Ruby",
          swift: "Swift",
          kotlin: "Kotlin",
          dart: "Dart",
        };

        return langMap[lang] || lang.toUpperCase();
      }

      return "Code";
    },

    /**
     * 格式化代码消息（提取并高亮代码）
     */
    formatCodeMessage(content) {
      if (!content) return "";

      // 提取代码块内容
      let code = content.replace(/```\w*\n?/g, "").replace(/```/g, "");

      // HTML 转义
      code = code
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");

      return code;
    },

    /**
     * 复制代码
     */
    copyCodeMessage(content) {
      if (!content) return;

      // 提取纯代码内容
      const code = content
        .replace(/```\w*\n?/g, "")
        .replace(/```/g, "")
        .trim();

      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard
          .writeText(code)
          .then(() => {
            this.$message.success("代码已复制到剪贴板");
          })
          .catch(() => {
            this.fallbackCopy(code);
          });
      } else {
        this.fallbackCopy(code);
      }
    },

    /**
     * 下载代码
     */
    downloadCodeMessage(content) {
      if (!content) return;

      const code = content
        .replace(/```\w*\n?/g, "")
        .replace(/```/g, "")
        .trim();
      const language = this.detectCodeLanguage(content).toLowerCase();

      // 根据语言确定文件扩展名
      const extMap = {
        javascript: "js",
        typescript: "ts",
        python: "py",
        java: "java",
        sql: "sql",
        html: "html",
        css: "css",
        scss: "scss",
        json: "json",
        xml: "xml",
        bash: "sh",
        shell: "sh",
        vue: "vue",
        jsx: "jsx",
        tsx: "tsx",
        go: "go",
        rust: "rs",
        c: "c",
        "c++": "cpp",
        "c#": "cs",
        php: "php",
        ruby: "rb",
        swift: "swift",
        kotlin: "kt",
        dart: "dart",
      };

      const ext = extMap[language] || "txt";
      const filename = `code_${Date.now()}.${ext}`;

      // 创建下载链接
      const blob = new Blob([code], { type: "text/plain;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);

      this.$message.success("代码已下载");
    },

    /**
     * 预览文件
     */
    previewFile(file) {
      if (!file) return;

      // 如果有 URL，直接在新窗口打开
      if (file.url) {
        window.open(file.url, "_blank");
      } else if (file.raw) {
        // 如果有原始文件，创建临时 URL 并打开
        const url = URL.createObjectURL(file.raw);
        window.open(url, "_blank");
        // 延迟释放 URL（给浏览器时间打开）
        setTimeout(() => URL.revokeObjectURL(url), 60000);
      } else {
        this.$message.warning("文件预览不可用");
      }
    },

    /**
     * 显示用户信息
     * 用户标识从后端获取，不允许手动修改
     */
    changeUserId() {
      this.$message.info("用户信息已从系统获取，无需手动设置");
    },

    toggleTheme() {
      // 主题切换功能（可选）
      console.log("主题切换");
    },

    handleLogin() {
      // 登录功能（可选）
      this.$router.push("/login");
    },
  },
  watch: {
    message() {
      this.$nextTick(() => {
        this.autoResizeTextarea();
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.page-wrapper {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.home-root {
  position: relative;
  width: 100%;
  height: calc(100% - 66px); /* 减去导航栏高度 */
  margin-top: 66px; /* 为固定导航栏留出空间 */
  display: flex;
  flex-direction: row;
  background: linear-gradient(135deg, #f5f7ff 0%, #f0f4ff 100%);
  --c-primary: #3b82f6;
  --c-primary-2: #2563eb;
  --c-accent: #0ea5e9;
  --c-bg: #f1f5f9;
  --c-card: #ffffff;
  --c-border: #cbd5e1;
  --c-muted: #64748b;
  --c-law-blue: #3b82f6;
  --c-law-dark: #2563eb;
  --c-law-gray: #475569;
  --font-family-fangsong: "FangSong", "仿宋_GB2312", "STFangsong", "SimSun", serif;
}

/* 增强玻璃拟态浮动元素 */
.home-root::before {
  content: "";
  position: absolute;
  top: 15%;
  left: 15%;
  width: 320px;
  height: 320px;
  background: linear-gradient(
      135deg,
      rgba(0, 82, 204, 0.7),
      rgba(0, 122, 255, 0.5)
  );
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.35;
  z-index: 0;
  animation: float 20s infinite alternate ease-in-out;
  pointer-events: none;
}

.home-root::after {
  content: "";
  position: absolute;
  bottom: 15%;
  right: 15%;
  width: 240px;
  height: 240px;
  background: linear-gradient(
      135deg,
      rgba(0, 102, 255, 0.6),
      rgba(0, 178, 255, 0.4)
  );
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.35;
  z-index: 0;
  animation: float 20s infinite alternate ease-in-out;
  animation-delay: -5s;
  pointer-events: none;
}

@keyframes float {
  0% {
    transform: translate(0, 0) scale(1);
  }
  100% {
    transform: translate(50px, 30px) scale(1.1);
  }
}

.home-root > .sidebar,
.home-root > .main-content {
  position: relative;
  z-index: 1;
}

/* 主内容区样式 */
.main-content {
  flex: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 20px;
  box-sizing: border-box;
}

/* 聊天容器 - 大框体样式（参考汇小二.html） */
.chat-container {
  flex: 1;
  background: rgba(255, 255, 255, 0.78);
  backdrop-filter: blur(14px) saturate(130%);
  -webkit-backdrop-filter: blur(14px) saturate(130%);
  border-radius: 24px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

/* 欢迎区域样式优化 */
.welcome-area {
  flex: 1;
  min-height: 300px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  position: relative;
  background: rgba(255, 255, 255, 0.25);
  padding-top: 80px;
  padding-bottom: 28px;
  padding-left: 28px;
  padding-right: 28px;
  overflow-y: auto;
}

.welcome-chat-container {
  width: 95%;
  max-width: 1500px;
  margin: 0 auto;
  padding: 0;
  animation: fadeIn 0.6s ease;
}

.welcome-message-wrapper {
  display: flex;
  justify-content: center;
  width: 100%;
}

.welcome-message-item {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: start;
  -ms-flex-align: start;
  align-items: flex-start;
  width: 100%;
  justify-content: center;
}

/* 欢迎气泡 */
.welcome-bubble {
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  border-radius: 24px;
  padding: 40px 40px;
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06),
  inset 0 0 0 1px rgba(255, 255, 255, 0.5);
  transition: all 0.3s ease;
  width: 100%;
  max-width: 1000px;
  display: flex;
  flex-direction: column;
}

.welcome-bubble > * + * {
  margin-top: 28px;
}

/* 上半部分：卡通形象和欢迎语并排 */
.welcome-top-section {
  display: flex;
  align-items: center;
  text-align: left;
  width: 100%;
  /*gap: 32px;*/
}

.welcome-top-section > * + * {
  margin-left: 32px;
}

/* 左侧卡通形象区域 */
.welcome-avatar-section {
  flex-shrink: 0;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-right: 32px;
}

.welcome-avatar-section > * + * {
  margin-left: 12px;
}

.welcome-avatar {
  position: relative;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: linear-gradient(135deg, #8d9ee9 0%, #46d8d6 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
  animation: float 6s ease-in-out infinite;
}

.welcome-avatar-image {
  width: 117px;
  height: 117px;
  border-radius: 50%;
  object-fit: cover;
  border: 4px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}



/* 右侧欢迎语区域 */
.welcome-greeting-section {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -ms-flex-direction: column;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  flex: 1;
}

.welcome-greeting-section > * + * {
  margin-top: 16px;
}

.welcome-greeting {
  font-size: 28px;
  font-weight: 700;
  background: linear-gradient(135deg, #0d47a1 0%, #1976d2 50%, #42a5f5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 1.4;
  text-align: left;
  margin: 0;
  letter-spacing: -0.3px;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.welcome-subtitle {
  font-size: 16px;
  color: #666;
  line-height: 1.5;
  text-align: left;
  max-width: 100%;
  margin: 0;
  font-weight: 400;
}

/* 分隔线 */
.welcome-divider {
  height: 1px;
  background: linear-gradient(
      to right,
      transparent,
      rgba(13, 71, 161, 0.15),
      transparent
  );
  margin: 8px 0;
}

/* 下半部分：提示问题和推荐问题 */
.welcome-bottom-section {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
}

.welcome-bottom-section > * + * {
  margin-top: 24px;
}

.welcome-tip-section {
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  font-size: 16px;
  color: #666;
  padding: 12px 0;
  text-align: left;
  align-self: flex-start;
}

.welcome-tip-section > * + * {
  margin-left: 8px;
}

.welcome-tip-section i {
  font-size: 14px;
  color: #0d47a1;
}

/* 推荐问题按钮优化 */
.welcome-suggestions {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: (minmax(280px, 1fr))[auto-fit];
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  grid-gap: 12px;

  width: 100%;
  max-width: 100%;
}

.welcome-suggestion-btn {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  padding: 14px 18px;
  background: rgba(255, 255, 255, 0.8);
  border: 1px solid rgba(13, 71, 161, 0.15);
  border-radius: 12px;
  cursor: pointer;
  -webkit-transition: all 0.3s ease;
  transition: all 0.3s ease;
  text-align: left;
  width: 100%;
  position: relative;
  overflow: hidden;
  margin-right: 12px;
}

.welcome-suggestion-btn > * + * {
  margin-left: 12px;
}

.welcome-suggestion-btn::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  background: linear-gradient(135deg, #0d47a1, #1976d2);
  transform: scaleY(0);
  transition: transform 0.3s ease;
}

.welcome-suggestion-btn:hover {
  background: rgba(255, 255, 255, 0.95);
  border-color: rgba(13, 71, 161, 0.3);
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(13, 71, 161, 0.15);
}

.welcome-suggestion-btn:hover::before {
  transform: scaleY(1);
}

.welcome-suggestion-btn i {
  font-size: 18px;
  color: #0d47a1;
  flex-shrink: 0;

}

.welcome-suggestion-btn .suggestion-icon {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  display: block;
}

.welcome-suggestion-btn span {
  font-size: 14px;
  color: #333;
  font-weight: 500;
  flex: 1;
  line-height: 1.4;
}

/* 问题文字调小 */
.welcome-suggestion-btn .question-text {
  font-size: 14px;
  color: #333;
  font-weight: 500;
  flex: 1;
  line-height: 1.4;
}

/* 变量提示 */
.welcome-variable-tip {
  display: flex;
  align-items: center;
  padding: 14px 20px;
  background: rgba(255, 243, 224, 0.8);
  border: 1px solid rgba(255, 152, 0, 0.2);
  border-radius: 12px;
  width: 100%;
  max-width: 100%;
  align-self: flex-start;
}

.welcome-variable-tip > * + * {
  margin-left: 8px;
}

.welcome-variable-tip i {
  font-size: 18px;
  color: #f57c00;
  flex-shrink: 0;
}

.welcome-variable-tip span {
  font-size: 14px;
  color: #e65100;
  line-height: 1.5;
}

/* 动画效果 */
@keyframes float {
  0%, 100% {
    transform: translateY(0px);
  }
  50% {
    transform: translateY(-10px);
  }
}

@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 响应式设计 */
@media (max-width: 768px) {
  .welcome-chat-container {
    width: 95%;
    padding: 0 8px;
  }

  .welcome-bubble {
    padding: 30px 20px;
  }

  .welcome-bubble > * + * {
    margin-top: 24px;
  }

  .welcome-top-section {
    flex-direction: column;
    text-align: center;
  }

  .welcome-top-section > * + * {
    margin-top: 24px;
  }

  .welcome-avatar {
    width: 100px;
    height: 100px;
  }

  .welcome-avatar-image {
    width: 85px;
    height: 85px;
  }

  .welcome-greeting-section {
    align-items: center;
    text-align: center;
  }

  .welcome-greeting {
    font-size: 24px;
    text-align: center;
  }

  .welcome-subtitle {
    font-size: 15px;
    text-align: center;
  }

  .welcome-suggestions {
    -ms-grid-columns: 1fr;
    grid-template-columns: 1fr;
    grid-gap: 10px;

  }

  .welcome-suggestion-btn {
    padding: 12px 16px;
    margin-right: 10px;
  }

  .welcome-tip-section {
    font-size: 15px;
    padding: 10px 0;
  }

  .welcome-suggestion-btn .question-text {
    font-size: 13px;
  }
}

@media (max-width: 480px) {
  .welcome-bubble {
    padding: 24px 16px;
  }

  .welcome-bubble > * + * {
    margin-top: 20px;
  }

  .welcome-avatar {
    width: 80px;
    height: 80px;
  }

  .welcome-avatar-image {
    width: 70px;
    height: 70px;
  }

  .welcome-greeting {
    font-size: 20px;
  }

  .welcome-subtitle {
    font-size: 14px;
  }

  .welcome-suggestion-btn {
    padding: 10px 14px;
  }

  .welcome-suggestion-btn i {
    font-size: 16px;
  }

  .welcome-suggestion-btn .question-text {
    font-size: 12px;
  }
}

/* 外层滚动容器：允许用户往上滑动查看之前的内容 */
.chat-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0; /* 允许flex子元素缩小 */
}

/* 自定义 chat-body 滚动条 */
.chat-body::-webkit-scrollbar {
  width: 8px;
}

.chat-body::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.03);
  border-radius: 4px;
}

.chat-body::-webkit-scrollbar-thumb {
  background: rgba(147, 197, 253, 0.4);
  border-radius: 4px;
}

.chat-body::-webkit-scrollbar-thumb:hover {
  background: rgba(147, 197, 253, 0.6);
}

/* 聊天区域 */
.chat-area {
  flex: 1;
  min-height: 300px;
  display: flex;
  flex-direction: column;
  position: relative;
  background: rgba(255, 255, 255, 0.25);
  padding: 28px;
}

.chat-area::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0);
  z-index: 0;
  pointer-events: none;
}

.messages-container {
  flex: 1;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  z-index: 1;
}

.message-wrapper {
  display: flex;
  margin-bottom: 28px;
  animation: fadeIn 0.6s ease;
  width: 100%;
  max-width: 900px;
  padding: 0 24px;
  box-sizing: border-box;
}

.message-wrapper.user {
  justify-content: flex-end;
}

.message-wrapper.assistant {
  justify-content: flex-start;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(15px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message-item {
  display: flex;
  max-width: 95%;
  min-width: 200px;
  align-items: flex-start;
  width: fit-content;
}

/* 响应式设计：超大屏幕时限制宽度 */
@media (min-width: 1400px) {
  .message-wrapper {
    max-width: 1000px;
  }

  .message-item {
    max-width: 95%;
  }
}

/* 中等屏幕 */
@media (max-width: 768px) {
  .message-wrapper {
    max-width: 100%;
    padding: 0 16px;
  }

  .message-item {
    max-width: 90%;
    min-width: auto;
  }
}

.message-avatar {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 20px;
  background: rgba(255, 255, 255, 0.75);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
  overflow: hidden;
  margin-right: 28px;
}

.avatar-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.message-wrapper.user .message-avatar {
  background: rgba(255, 255, 255, 0.75);
  color: #ffffff;
  margin-left: 28px;
}

.message-wrapper.assistant .message-avatar {
  background: rgba(255, 255, 255, 0.75);
  color: #3b82f6;
}

.message-content {
  flex: 0 1 auto;
  min-width: 0;
  width: fit-content;
}

.message-text {
  padding: 20px 25px;
  border-radius: 24px;
  line-height: 1.75;
  word-wrap: break-word;
  word-break: break-word;
  font-size: 16px;
  width: fit-content;
  max-width: 100%;
  min-width: 120px;
  height: auto;
  min-height: auto;
  transition: all 0.3s ease;
}

.message-wrapper.user .message-text {
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  color: #333;
  border-top-right-radius: 8px;
  border: 2px solid rgba(13, 71, 161, 0.15);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06),
  inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.message-wrapper.assistant .message-text {
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  color: #333;
  border: 2px solid rgba(13, 71, 161, 0.12);
  border-top-left-radius: 8px;
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06),
  inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.message-wrapper.user .message-text ::v-deep strong {
  color: #1e40af;
  font-weight: 600;
}

.message-wrapper.assistant .message-text ::v-deep strong {
  color: #111827;
  font-weight: 600;
}

/* 思考过程和回答内容的样式优化 */
.message-text ::v-deep .thinking-section {
  margin-bottom: 24px; /* 增加思考与回答之间的间距 */
  padding: 16px;
  background: #f8fafc;
  border-radius: 12px;
  border-left: 4px solid #9ca3af;
  font-family: var(--font-family-fangsong); /* 思考部分使用仿宋字体，突出“思考过程”质感 */
  font-size: 14px;
  line-height: 1.6;
  color: #4b5563;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.message-text ::v-deep .answer-section {
  font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans", sans-serif; /* 回答部分使用更优雅的字体 */
  font-size: 16px;
  line-height: 1.8;
  color: #1f2937;
  font-weight: 400;
  padding: 8px 0;
  background: transparent;
  animation: fadeInUp 0.5s ease-out;
}

/* 回答区域在“思考尚未锁定”阶段，其实是在流式输出思考文案，此时也使用仿宋字体；
   一旦识别到 </think> 并锁定思考块（msg.thinkingLocked = true），回答区域恢复为默认字体。 */
.message-text ::v-deep .answer-section.answer-thinking-flow {
  font-family: var(--font-family-fangsong);
}

/* 思考过程标题样式 */
.message-text ::v-deep .thinking-section .think-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid #e5e7eb;
  font-size: 13px;
  font-weight: 600;
  color: #6b7280;
  cursor: pointer;
  transition: all 0.2s ease;
}

.message-text ::v-deep .thinking-section .think-header:hover {
  color: #4b5563;
  background: rgba(0, 0, 0, 0.02);
  border-radius: 6px;
  padding: 4px 8px;
  margin: -4px -8px 8px -8px;
}

.message-text ::v-deep .thinking-section .think-header i:first-child {
  font-size: 16px;
  color: #9ca3af;
}

.message-text ::v-deep .thinking-section .think-toggle-icon {
  font-size: 14px;
  color: #9ca3af;
  transition: transform 0.2s ease;
  margin-left: auto;
}

/* 思考内容样式 */
.message-text ::v-deep .thinking-section .think-content {
  font-size: 16px;
  line-height: 1.6;
  color: #6b7280;
  transition: all 0.3s ease;
  overflow: hidden;
  animation: fadeInUp 0.3s ease-out;
}

.message-text ::v-deep .thinking-section .think-content.collapsed {
  max-height: 0;
  opacity: 0;
  margin-bottom: 0;
}

.message-text ::v-deep .thinking-section .think-content:not(.collapsed) {
  max-height: 1000px; /* 足够大的值以容纳内容 */
  opacity: 1;
}

/* 思考过程正文：和回答一样，去掉段前段后空白，每段首行缩进 */
.message-text ::v-deep .thinking-section p {
  margin: 0;
  text-indent: 0;  /* 思考过程正文取消首行缩进，统一左对齐 */
}

/* 思考中的编号段落，同样保持左对齐 */
.message-text ::v-deep .thinking-section p.no-indent {
  text-indent: 0;
}

/* 回答内容中的标题和重点突出 */
/* 回答内容中的标题样式（对齐 AIDialog.vue 的公文风格） */
.message-text ::v-deep .answer-section h1,
.message-text ::v-deep .answer-section h2,
.message-text ::v-deep .answer-section h3,
.message-text ::v-deep .answer-section h4,
.message-text ::v-deep .answer-section h5,
.message-text ::v-deep .answer-section h6 {
  margin: 20px 0 12px 0;
  line-height: 1.4;
}

/* 一级标题：方正小标宋，小二，居中 */
.message-text ::v-deep .answer-section h1 {
  font-family: "方正小标宋_GBK", "SimSun", "宋体", serif;
  font-size: 24px;
  font-weight: 700;
  text-align: center;
  border: none;
  padding: 0.3em 0 0.3em 0;
}

/* 二级标题：黑体，三号，左对齐 */
.message-text ::v-deep .answer-section h2 {
  font-family: "黑体", "SimHei", "Microsoft YaHei", sans-serif;
  font-size: 22px;
  font-weight: 650;
  padding-bottom: 0.25em;
  margin: 1em 0 1em 0.9em;
  border: none;
}

/* 三级标题：略小一点，左对齐 */
.message-text ::v-deep .answer-section h3 {
  font-size: 20px;
  font-weight: 620;
  margin: 0.2em -1.2em 0.2em -1.2em;
  position: relative;
  padding-left: 15px;
  color: #374151;
}

.message-text ::v-deep .answer-section h4 {
  font-size: 20px;
  font-weight: 600;
  margin: 0.2em 0 0.2em 0.2em;
  color: #2c3e50;
}

.message-text ::v-deep .answer-section strong {
  font-weight: 600;
  color: #1e40af;
  background: linear-gradient(120deg, #bfdbfe 0%, #bfdbfe 100%);
  background-repeat: no-repeat;
  background-size: 100% 0.2em;
  background-position: 0 88%;
}

.message-text ::v-deep .answer-section p {
  margin: 0;               /* 不要段落之间额外空行，只保留行高 */
  text-indent: 0;          /* 取消首行缩进，整体左对齐 */
}

/* 编号段落（如 1. 2. 3.），保持左侧对齐（与普通段落一致） */
.message-text ::v-deep .answer-section p.no-indent {
  text-indent: 0;
}

.message-text ::v-deep .answer-section ul,
.message-text ::v-deep .answer-section ol {
  margin: 12px 0;
  padding-left: 24px;
}

.message-text ::v-deep .answer-section li {
  margin: 6px 0;
  line-height: 1.6;
}

/* 代码块在回答中的样式 */
.message-text ::v-deep .answer-section .code-block {
  background: #1f2937;
  color: #f9fafb;
  padding: 20px;
  border-radius: 8px;
  margin: 16px 0;
  overflow-x: auto;
  font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
  font-size: 14px;
  line-height: 1.5;
  border: 1px solid #374151;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.message-text ::v-deep .answer-section .inline-code {
  background: rgba(59, 130, 246, 0.1);
  padding: 2px 6px;
  border-radius: 4px;
  font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
  font-size: 14px;
  color: #1e40af;
  border: 1px solid rgba(59, 130, 246, 0.2);
}

/* 思考过程中的代码和表格样式（更简洁） */
.message-text ::v-deep .thinking-section .code-block {
  background: #f3f4f6;
  color: #374151;
  padding: 12px;
  border-radius: 6px;
  margin: 8px 0;
  overflow-x: auto;
  font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
  font-size: 13px;
  line-height: 1.4;
  border: 1px solid #e5e7eb;
}

.message-text ::v-deep .thinking-section .inline-code {
  background: #e5e7eb;
  padding: 1px 4px;
  border-radius: 3px;
  font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
  font-size: 13px;
  color: #374151;
}

.message-text ::v-deep .code-block {
  background: #f8fafc;
  color: #334155;
  padding: 28px 32px;
  border-radius: 8px;
  margin: 12px 0;
  overflow-x: auto;
  font-family: var(--font-family-code);
  font-size: var(--font-size-text-normal);
  line-height: var(--line-height-relaxed);
  border: 1px solid #e2e8f0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}

.message-text ::v-deep .inline-code {
  background: rgba(59, 130, 246, 0.1);
  padding: 2px 6px;
  border-radius: 4px;
  font-family: var(--font-family-code);
  font-size: var(--font-size-text-small);
  color: #1e40af;
}

.message-wrapper.assistant .message-text ::v-deep .inline-code {
  background: #f1f5f9;
  color: #e11d48;
}

.message-text ::v-deep ul {
  margin: 8px 0;
  padding-left: 20px;
}

.message-text ::v-deep li {
  margin: 4px 0;
}

/* Markdown 表格样式 - 现代蓝色主题 */
.message-text ::v-deep .markdown-table-wrapper {
  margin: 12px 0;
  overflow-x: auto;
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.12);
  border: 1px solid rgba(59, 130, 246, 0.1);
}

.message-text ::v-deep .markdown-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: #ffffff;
  font-size: 14px;
  line-height: 1.6;
}

.message-text ::v-deep .markdown-table thead {
  background-color: #3b82f6;
  color: #ffffff;
}

.message-text ::v-deep .markdown-table thead th {
  padding: 12px 16px;
  text-align: center;
  font-weight: 600;
  font-size: 14px;
  white-space: nowrap;
}

.message-text ::v-deep .markdown-table thead th:first-child {
  border-top-left-radius: 12px;
}

.message-text ::v-deep .markdown-table thead th:last-child {
  border-top-right-radius: 12px;
}

.message-text ::v-deep .markdown-table tbody tr {
  transition: all 0.3s ease;
  border-bottom: 1px solid #e5e7eb;
}

.message-text ::v-deep .markdown-table tbody tr:nth-child(odd) {
  background-color: #ffffff;
}

.message-text ::v-deep .markdown-table tbody tr:nth-child(even) {
  background-color: #f8fafc;
}

.message-text ::v-deep .markdown-table tbody tr:hover {
  background: linear-gradient(90deg, #eff6ff 0%, #dbeafe 100%);
  transform: scale(1.01);
  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
}

.message-text ::v-deep .markdown-table td,
.message-text ::v-deep .markdown-table tbody th {
  padding: 12px 16px;
  border-bottom: 1px solid #e5e7eb;
  color: #1f2937;
  vertical-align: middle;
  text-align: center;
  transition: all 0.2s ease;
}

.message-text ::v-deep .markdown-table tbody th {
  font-weight: 600;
  background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
  color: #1e40af;
  text-align: center;
  border-right: 3px solid #3b82f6;
}

.message-text ::v-deep .markdown-table tbody tr:last-child td,
.message-text ::v-deep .markdown-table tbody tr:last-child th {
  border-bottom: none;
}

.message-text ::v-deep .markdown-table tbody tr:last-child td:first-child {
  border-bottom-left-radius: 12px;
}

.message-text ::v-deep .markdown-table tbody tr:last-child td:last-child {
  border-bottom-right-radius: 12px;
}

/* 用户消息中的表格样式调整 */
.message-wrapper.user .message-text ::v-deep .markdown-table-wrapper {
  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.18);
  border: 1px solid rgba(59, 130, 246, 0.2);
}

.message-wrapper.user .message-text ::v-deep .markdown-table thead {
  background-color: #2563eb;
}

.message-wrapper.user .message-text ::v-deep .markdown-table tbody tr:hover {
  background: linear-gradient(90deg, #dbeafe 0%, #bfdbfe 100%);
}

/* 思考过程中的表格样式 */
.message-text ::v-deep .thinking-section .markdown-table-wrapper {
  margin: 8px 0;
  overflow-x: auto;
  border-radius: 6px;
  border: 1px solid #e5e7eb;
}

.message-text ::v-deep .thinking-section .markdown-table {
  font-size: 13px;
}

.message-text ::v-deep .thinking-section .markdown-table thead {
  background: #6b7280;
}

.message-text ::v-deep .thinking-section .markdown-table thead th {
  padding: 10px 12px;
}

.message-text ::v-deep .thinking-section .markdown-table td {
  padding: 8px 12px;
}

/* 回答内容中的表格样式 */
.message-text ::v-deep .answer-section .markdown-table-wrapper {
  margin: 20px 0;
  overflow-x: auto;
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.08);
  border: 1px solid rgba(59, 130, 246, 0.1);
}

.message-text ::v-deep .answer-section .markdown-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: #ffffff;
  font-size: 14px;
  line-height: 1.6;
}

.message-text ::v-deep .answer-section .markdown-table thead {
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  color: #ffffff;
}

.message-text ::v-deep .answer-section .markdown-table thead th {
  padding: 14px 16px;
  text-align: center;
  font-weight: 600;
  font-size: 14px;
  white-space: nowrap;
  border-bottom: none;
}

.message-text ::v-deep .answer-section .markdown-table tbody tr {
  transition: all 0.3s ease;
  border-bottom: 1px solid #f3f4f6;
}

.message-text ::v-deep .answer-section .markdown-table tbody tr:hover {
  background: linear-gradient(90deg, #eff6ff 0%, #dbeafe 100%);
  transform: translateX(4px);
  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
}

.message-text ::v-deep .answer-section .markdown-table td {
  padding: 12px 16px;
  border-bottom: 1px solid #f3f4f6;
  color: #374151;
  vertical-align: middle;
  text-align: center;
}

/* 回答中的分隔线（由 --- 等 Markdown 分隔线转换而来） */
.message-text ::v-deep .answer-section hr.ai-hr-separator {
  border: none;
  border-top: 1px dashed #e5e7eb;
  margin: 16px 0;
}

/* 响应式表格 */
@media (max-width: 768px) {
  .message-text ::v-deep .markdown-table-wrapper {
    font-size: 12px;
    border-radius: 8px;
  }

  .message-text ::v-deep .markdown-table thead th,
  .message-text ::v-deep .markdown-table td,
  .message-text ::v-deep .markdown-table tbody th {
    padding: 10px 12px;
  }

  .message-text ::v-deep .markdown-table tbody tr:hover {
    transform: none;
  }

  .message-text ::v-deep .thinking-section {
    margin-bottom: 20px;
    padding: 12px;
    font-size: 13px;
  }

  .message-text ::v-deep .answer-section {
    font-size: 15px;
    line-height: 1.7;
  }

  .message-text ::v-deep .answer-section h1 {
    font-size: 18px;
  }

  .message-text ::v-deep .answer-section h2 {
    font-size: 16px;
  }

  .message-text ::v-deep .answer-section h3 {
    font-size: 15px;
  }
}

/* 动画效果 */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message-time {
  font-size: var(--font-size-text-mini);
  color: #9ca3af;
  margin-top: 4px;
  padding: 0 4px;
  margin-right: 6px;
}

.message-wrapper.user .message-time {
  text-align: right;
}

/* 消息操作按钮区域 */
.message-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;

  margin-top: 6px;
}

.action-buttons {
  display: flex;
  align-items: center;

  opacity: 0;
  transition: opacity 0.2s;
}

.message-wrapper:hover .action-buttons {
  opacity: 1;
}

.action-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  color: #6b7280;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 2px;
}

.action-btn:hover:not(:disabled) {
  background: #f3f4f6;
  border-color: #3b82f6;
  color: #3b82f6;
  transform: translateY(-1px);
}

.action-btn:active:not(:disabled) {
  transform: translateY(0);
}

.action-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.action-btn i {
  font-size: 14px;
}

/* 变量输入面板 */
.variables-panel {
  border-top: 1px solid #e5e7eb;
  background: #ffffff;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  cursor: pointer;
  transition: background 0.2s;
  border-bottom: 1px solid #e5e7eb;
}

.panel-header:hover {
  background: #f9fafb;
}

.header-left {
  display: flex;
  align-items: center;

}

.header-left i {
  font-size: 18px;
  color: #3b82f6;
  margin-right: 12px;
}

.panel-title {
  font-size: 15px;
  font-weight: 600;
  color: #374151;
}

.variable-count {
  font-size: 13px;
  color: #9ca3af;
  background: #f3f4f6;
  padding: 2px 10px;
  border-radius: 12px;
}

.panel-content {
  padding: 20px 24px;
  max-height: 200px;
  overflow-y: auto;
  background: #f9fafb;

  &::-webkit-scrollbar {
    width: 6px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
  }

  &::-webkit-scrollbar-thumb {
    background: #d1d5db;
    border-radius: 3px;
  }

  &::-webkit-scrollbar-thumb:hover {
    background: #9ca3af;
  }
}

/* 输入栏内参数弹窗内容 */
.composer-variables {
  max-height: 380px;
  overflow-y: auto;
  padding: 16px;
  /* 兼容老版本浏览器的渐变语法 */
  background: #f0f8ff; /* 降级方案 */
  background: -webkit-linear-gradient(315deg, #f0f8ff 0%, #e6f2ff 100%);
  background: -moz-linear-gradient(315deg, #f0f8ff 0%, #e6f2ff 100%);
  background: -o-linear-gradient(315deg, #f0f8ff 0%, #e6f2ff 100%);
  background: linear-gradient(135deg, #f0f8ff 0%, #e6f2ff 100%);
  border-radius: 10px;
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
  box-shadow: 0 3px 6px rgba(59, 130, 246, 0.08);
  -webkit-box-shadow: 0 3px 6px rgba(59, 130, 246, 0.08);
  -moz-box-shadow: 0 3px 6px rgba(59, 130, 246, 0.08);

  &::-webkit-scrollbar {
    width: 6px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
  }

  &::-webkit-scrollbar-thumb {
    background: #cbd5e1;
    border-radius: 3px;
    -webkit-border-radius: 3px;
  }

  &::-webkit-scrollbar-thumb:hover {
    background: #94a3b8;
  }

  .empty-state {
    text-align: center;
    color: #64748b;
    padding: 32px 20px;
    font-size: 14px;
    border-radius: 10px;
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
    background: white;
    border: 1px solid #e2e8f0;
  }

  .variables-form {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -webkit-flex-direction: column;
    -moz-box-orient: vertical;
    -moz-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
    gap: 10px;

    /* 为不支持gap的旧浏览器提供后备方案 */
    > * + * {
      margin-top: 10px;
    }
  }

  .variable-item {
    /* 浅蓝色渐变背景 */
    background: #fefefe; /* 降级方案 */
    background: -webkit-linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
    background: -moz-linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
    background: -o-linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
    background: linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
    border-radius: 8px;
    -webkit-border-radius: 8px;
    -moz-border-radius: 8px;
    padding: 14px 16px;
    border: 1px solid #e1ecf4;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
    margin-bottom: 12px; /* 变量项之间的间距 */
    box-shadow: 0 1px 3px rgba(59, 130, 246, 0.06);
    -webkit-box-shadow: 0 1px 3px rgba(59, 130, 246, 0.06);
    -moz-box-shadow: 0 1px 3px rgba(59, 130, 246, 0.06);

    &:last-child {
      margin-bottom: 0; /* 最后一项不需要底部间距 */
    }

    &:hover {
      background: #f0f7ff; /* 降级方案 */
      background: -webkit-linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
      background: -moz-linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
      background: -o-linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
      background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
      border-color: #bfdbfe;
      box-shadow: 0 3px 8px rgba(59, 130, 246, 0.12);
      -webkit-box-shadow: 0 3px 8px rgba(59, 130, 246, 0.12);
      -moz-box-shadow: 0 3px 8px rgba(59, 130, 246, 0.12);
      -webkit-transform: translateY(-1px);
      -moz-transform: translateY(-1px);
      -ms-transform: translateY(-1px);
      -o-transform: translateY(-1px);
      transform: translateY(-1px);
    }
  }

  .variable-row {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    /* 移除 gap（Chrome 75-77 不支持） */
  }

  /* Chrome 75-77 兼容：用 margin-left 替代 gap */
  .variable-row > .variable-input-area {
    margin-left: 12px;
  }

  .variable-label {
    font-size: 14px;
    font-weight: 600;
    color: #1e40af;
    min-width: 70px;
    max-width: 70px;
    -webkit-flex-shrink: 0;
    -ms-flex-negative: 0;
    flex-shrink: 0;
    line-height: 1.4;
    text-align: left; /* 改为左对齐，更整齐 */
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    white-space: nowrap; /* 防止标签换行 */

    .required-mark {
      color: #ef4444;
      font-weight: bold;
      margin-left: 3px;
      font-size: 16px; /* 稍微大一点，更醒目 */
    }
  }

  .variable-input-area {
    -webkit-box-flex: 1;
    -webkit-flex: 1;
    -moz-box-flex: 1;
    -ms-flex: 1;
    flex: 1;
    min-width: 0;
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -webkit-flex-direction: column;
    -moz-box-orient: vertical;
    -moz-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
  }

  .variable-input {
    width: 100%;
    padding: 7px 10px;
    border: 1px solid #d1e7ff;
    border-radius: 6px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
    font-size: 13px;
    font-family: inherit;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
    background: #fdfdff;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;

    &:focus {
      outline: none;
      border-color: #3b82f6;
      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
      -webkit-box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
      -moz-box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
      background: white;
    }

    &:hover {
      border-color: #93c5fd;
      background: white;
    }

    &.textarea {
      resize: vertical;
      min-height: 60px;
    }

    &::placeholder {
      color: #9ca3af;
    }

    &::-webkit-input-placeholder {
      color: #9ca3af;
    }

    &::-moz-placeholder {
      color: #9ca3af;
      opacity: 1;
    }

    &:-ms-input-placeholder {
      color: #9ca3af;
    }
  }

  .file-upload-wrapper,
  .file-list-wrapper {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -webkit-flex-direction: column;
    -moz-box-orient: vertical;
    -moz-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
    width: 100%;
    /* 移除 gap（Chrome 75-77 不支持） */
  }

  /* Chrome 75-77 兼容：用 margin-top 替代 gap */
  .file-upload-wrapper > * + *,
  .file-list-wrapper > * + * {
    margin-top: 8px;
  }

  .hidden-file {
    position: absolute;
    left: -9999px;
    opacity: 0;
    width: 0.1px;
    height: 0.1px;
    overflow: hidden;
    z-index: -1;
  }

  .file-control-row {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    /* 移除 gap（Chrome 75-77 不支持） */
  }

  /* Chrome 75-77 兼容：用 margin-left 替代 gap */
  .file-control-row > * + * {
    margin-left: 8px;
  }

  .file-display-row {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    width: 100%;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    /* 移除 gap（Chrome 75-77 不支持） */
    padding: 8px 12px;
    background: #f8fafc;
    background: -webkit-gradient(linear, left top, right bottom, from(#f8fafc), to(#ffffff));
    background: -o-linear-gradient(top left, #f8fafc, #ffffff);
    background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
    border: 1px solid #e2e8f0;
    border-radius: 6px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
  }

  /* Chrome 75-77 兼容：用 margin-left 替代 gap */
  .file-display-row > * + * {
    margin-left: 10px;
  }

  .file-upload-trigger {
    display: -webkit-inline-box;
    display: -webkit-inline-flex;
    display: -moz-inline-box;
    display: -ms-inline-flexbox;
    display: inline-flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    /* 移除 gap（Chrome 75-77 不支持） */
    padding: 8px 14px;
    /* 兼容老版本浏览器的渐变语法 */
    background: #dbeafe; /* 降级方案 */
    background: -webkit-linear-gradient(315deg, #dbeafe 0%, #bfdbfe 100%);
    background: -moz-linear-gradient(315deg, #dbeafe 0%, #bfdbfe 100%);
    background: -o-linear-gradient(315deg, #dbeafe 0%, #bfdbfe 100%);
    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
    border: 1px solid #93c5fd;
    border-radius: 6px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
    color: #1d4ed8;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);

    &:hover {
      background: #bfdbfe; /* 降级方案 */
      background: -webkit-linear-gradient(315deg, #bfdbfe 0%, #93c5fd 100%);
      background: -moz-linear-gradient(315deg, #bfdbfe 0%, #93c5fd 100%);
      background: -o-linear-gradient(315deg, #bfdbfe 0%, #93c5fd 100%);
      background: linear-gradient(135deg, #bfdbfe 0%, #93c5fd 100%);
      border-color: #60a5fa;
      -webkit-transform: translateY(-1px);
      -moz-transform: translateY(-1px);
      -ms-transform: translateY(-1px);
      -o-transform: translateY(-1px);
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    &:active {
      -webkit-transform: translateY(0);
      -moz-transform: translateY(0);
      -ms-transform: translateY(0);
      -o-transform: translateY(0);
      transform: translateY(0);
    }

    i {
      font-size: 14px;
      margin-right: 5px; /* 图标和文字之间的间距 */
    }
  }

  .file-remove-btn {
    display: -webkit-inline-box;
    display: -webkit-inline-flex;
    display: -moz-inline-box;
    display: -ms-inline-flexbox;
    display: inline-flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    -webkit-box-pack: center;
    -webkit-justify-content: center;
    -moz-box-pack: center;
    -ms-flex-pack: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    /* 兼容老版本浏览器的渐变语法 */
    background: #fee2e2; /* 降级方案 */
    background: -webkit-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
    background: -moz-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
    background: -o-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
    background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
    border: 1px solid #f87171;
    border-radius: 6px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
    color: #dc2626;
    cursor: pointer;
    font-weight: 500;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);

    &:hover {
      background: #fecaca; /* 降级方案 */
      background: -webkit-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
      background: -moz-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
      background: -o-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
      background: linear-gradient(135deg, #fecaca 0%, #f87171 100%);
      border-color: #ef4444;
      color: #b91c1c;
      -webkit-transform: translateY(-1px);
      -moz-transform: translateY(-1px);
      -ms-transform: translateY(-1px);
      -o-transform: translateY(-1px);
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    &:active {
      -webkit-transform: translateY(0);
      -moz-transform: translateY(0);
      -ms-transform: translateY(0);
      -o-transform: translateY(0);
      transform: translateY(0);
    }

    i {
      font-size: 14px;
    }
  }

  .file-name {
    font-size: 13px;
    color: #4b5563;
    padding: 8px 12px;
    /* 兼容老版本浏览器的渐变语法 */
    background: #f8fafc; /* 降级方案 */
    background: -webkit-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: -moz-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: -o-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
    border: 1px solid #e2e8f0;
    border-radius: 6px;
    -webkit-border-radius: 6px;
    -moz-border-radius: 6px;
    word-break: break-word;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    /* 移除 gap（Chrome 75-77 不支持） */
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
    -webkit-box-flex: 1;
    -webkit-flex: 1;
    -moz-box-flex: 1;
    -ms-flex: 1;
    flex: 1;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
  }

  /* Chrome 75-77 兼容：用 margin-left 替代 gap */
  .file-name > * + * {
    margin-left: 7px;

    &.clickable {
      cursor: pointer;

      &:hover {
        background: #f1f5f9; /* 降级方案 */
        background: -webkit-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
        background: -moz-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
        background: -o-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
        background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
        border-color: #cbd5e1;
        -webkit-transform: translateY(-1px);
        -moz-transform: translateY(-1px);
        -ms-transform: translateY(-1px);
        -o-transform: translateY(-1px);
        transform: translateY(-1px);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
        -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
        -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
      }
    }

    i {
      color: #64748b;
      font-size: 14px;
    }
  }

  .file-list {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -webkit-flex-direction: column;
    -moz-box-orient: vertical;
    -moz-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
    gap: 6px;

    /* 为不支持gap的旧浏览器提供后备方案 */
    > * + * {
      margin-top: 6px;
    }
  }

  .file-item {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -webkit-align-items: center;
    -moz-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    -webkit-box-pack: justify;
    -webkit-justify-content: space-between;
    -moz-box-pack: justify;
    -ms-flex-pack: justify;
    justify-content: space-between;
    padding: 10px 12px;
    /* 兼容老版本浏览器的渐变语法 */
    background: #f8fafc; /* 降级方案 */
    background: -webkit-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: -moz-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: -o-linear-gradient(315deg, #f8fafc 0%, #f1f5f9 100%);
    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    -webkit-border-radius: 8px;
    -moz-border-radius: 8px;
    font-size: 12px;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);

    &:hover {
      background: #f1f5f9; /* 降级方案 */
      background: -webkit-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
      background: -moz-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
      background: -o-linear-gradient(315deg, #f1f5f9 0%, #e2e8f0 100%);
      background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
      border-color: #cbd5e1;
      -webkit-transform: translateY(-1px);
      -moz-transform: translateY(-1px);
      -ms-transform: translateY(-1px);
      -o-transform: translateY(-1px);
      transform: translateY(-1px);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
      -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
      -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
    }

    .file-info {
      display: -webkit-box;
      display: -webkit-flex;
      display: -moz-box;
      display: -ms-flexbox;
      display: flex;
      -webkit-box-align: center;
      -webkit-align-items: center;
      -moz-box-align: center;
      -ms-flex-align: center;
      align-items: center;
      gap: 6px;
      -webkit-box-flex: 1;
      -webkit-flex: 1;
      -moz-box-flex: 1;
      -ms-flex: 1;
      flex: 1;
      min-width: 0;

      /* 为不支持gap的旧浏览器提供后备方案 */
      > * + * {
        margin-left: 6px;
      }

      i {
        color: #6b7280;
        font-size: 14px;
        -webkit-flex-shrink: 0;
        -ms-flex-negative: 0;
        flex-shrink: 0;
      }

      span {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: #4b5563;
      }
    }

    .file-remove-btn {
      width: 26px;
      height: 26px;
      /* 兼容老版本浏览器的渐变语法 */
      background: #fee2e2; /* 降级方案 */
      background: -webkit-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
      background: -moz-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
      background: -o-linear-gradient(315deg, #fee2e2 0%, #fecaca 100%);
      background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
      border: 1px solid #f87171;
      border-radius: 6px;
      -webkit-border-radius: 6px;
      -moz-border-radius: 6px;
      color: #dc2626;
      cursor: pointer;
      -webkit-transition: all 0.3s ease;
      -moz-transition: all 0.3s ease;
      -o-transition: all 0.3s ease;
      transition: all 0.3s ease;
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
      -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
      -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);

      &:hover {
        background: #fecaca; /* 降级方案 */
        background: -webkit-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
        background: -moz-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
        background: -o-linear-gradient(315deg, #fecaca 0%, #f87171 100%);
        background: linear-gradient(135deg, #fecaca 0%, #f87171 100%);
        color: #b91c1c;
        -webkit-transform: translateY(-1px);
        -moz-transform: translateY(-1px);
        -ms-transform: translateY(-1px);
        -o-transform: translateY(-1px);
        transform: translateY(-1px);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
        -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
        -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
      }

      i {
        font-size: 12px;
      }
    }
  }
}

/* 输入参数按钮强化样式 */
.parameters-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  position: relative;
  color: #409eff; /* 默认蓝色 */
  transition: color 0.3s ease;
}

.parameters-btn .btn-text {
  font-size: 12px;
}

.parameters-btn .badge {
  position: absolute;
  top: -8px;
  right: -8px;
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  color: #ffffff;
  border-radius: 10px;
  height: 20px;
  min-width: 20px;
  line-height: 20px;
  text-align: center;
  font-size: 12px;
  font-weight: 600;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  padding: 0 4px;
  box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4), 0 1px 2px rgba(0, 0, 0, 0.1);
  -webkit-box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4), 0 1px 2px rgba(0, 0, 0, 0.1);
  -moz-box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4), 0 1px 2px rgba(0, 0, 0, 0.1);
  border: 2px solid #ffffff;
  z-index: 100;
  letter-spacing: -0.5px;
}

/* 未填写必填项时显示红色 */
.parameters-btn.alert {
  color: #ef4444;
}

/* 移除重复的 variable-item 定义，已在上面 .composer-variables 中定义 */

/* 移除重复的 variable-label 定义，已在上面 .composer-variables 中定义 */

.required-mark {
  background: #fee2e2;
  color: #dc2626;
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 4px;
}

.optional-mark {
  background: #f3f4f6;
  color: #6b7280;
  font-size: 12px;
  padding: 2px 8px;
  border-radius: 4px;
}

.file-upload-area,
.file-list-upload-area {
  background: #ffffff;
  border: 1px dashed #d1d5db;
  border-radius: 8px;
  padding: 16px;
}

.upload-placeholder {
  display: flex;
  align-items: center;
  gap: 12px;
}

.upload-hint {
  font-size: 12px;
  color: #9ca3af;
}

.file-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: #f9fafb;
  border: 1px solid #e5e7eb;
  border-radius: 8px;

  i.el-icon-document {
    font-size: 24px;
    color: #3b82f6;
  }
}

.file-info {
  flex: 1;
  overflow: hidden;
}

.file-name {
  font-size: 13px;
  color: #374151;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-size {
  font-size: 12px;
  color: #9ca3af;
  margin-top: 2px;
}

.file-list {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* 输入区域样式优化 */
.input-area {
  padding: 20px 24px;
  border-top: 1px solid rgba(59, 130, 246, 0.15);
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  position: relative;
  flex-shrink: 0;
  box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.06);
}

.input-area::before {
  content: "";
  position: absolute;
  top: 0;
  left: 10%;
  width: 80%;
  height: 1px;
  background: linear-gradient(
      to right,
      transparent,
      rgba(59, 130, 246, 0.2),
      transparent
  );
}

.input-wrapper {
  width: 95%;
  margin: 0 auto;
}

.composer {
  width: 100%;
  background: #ffffff;
  border: 2px solid #e2e8f0;
  border-radius: 16px;
  padding: 16px 20px;
  transition: all 0.3s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  position: relative;
}

.composer:focus-within {
  border-color: #3b82f6;
  box-shadow: 0 6px 20px rgba(59, 130, 246, 0.15),
  0 0 0 3px rgba(59, 130, 246, 0.1);
  transform: translateY(-1px);
}

/* 附件区域样式优化 */
.attachments {
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid #f1f5f9;
}

.attachments-header {
  display: flex;
  align-items: center;

  margin-bottom: 12px;
  padding: 8px 12px;
  background: #f8fafc;
  border-radius: 8px;
  border-left: 3px solid #3b82f6;
}

.attachments-header i {
  font-size: 16px;
  color: #3b82f6;
  margin-right: 8px;
}

.attachments-count {
  font-size: 14px;
  font-weight: 600;
  color: #374151;
}

.attachment-list {
  display: flex;

  flex-wrap: wrap;
}

.attachment-card {
  display: flex;
  align-items: center;

  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  padding: 10px 14px;
  position: relative;
  max-width: 240px;
  min-width: 180px;
  transition: all 0.2s ease;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
  margin-right: 10px;
}

.attachment-card:hover {
  border-color: #3b82f6;
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.12);
  transform: translateY(-1px);
}

.file-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
  color: #3b82f6;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  flex-shrink: 0;
  border: 1px solid rgba(59, 130, 246, 0.2);
  margin-right: 10px;
}

.file-meta {
  flex: 1;
  overflow: hidden;
  min-width: 0;
}

.file-name {
  font-size: 13px;
  font-weight: 600;
  color: #1f2937;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 2px;
}

.file-size {
  font-size: 11px;
  color: #6b7280;
  font-weight: 500;
}

.att-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid #ffffff;
  background: #ef4444;
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s ease;
  box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3);
}

.att-remove:hover {
  background: #dc2626;
  transform: scale(1.1);
}

/* 输入行样式优化 */
.input-row {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  margin-bottom: 8px;
}

.input {
  flex: 1;
  min-height: 24px;
  max-height: 120px;
  border: none;
  outline: none;
  font-size: 15px;
  color: #111827;
  background: transparent;
  resize: none;
  font-family: inherit;
  line-height: 1.6;
  padding: 8px 0;
}

.input::placeholder {
  color: #9ca3af;
  font-weight: 400;
}

.input:focus {
  outline: none;
}

.input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* 操作按钮区域优化 */
.actions {
  display: flex;
  align-items: center;
  /* 旧版 Chrome (如 75-77) 不支持 flex-gap，这里使用 margin 方式做兼容 */
  gap: 0;
  flex-shrink: 0;
}

/* 通过相邻兄弟选择器增加间距，兼容不支持 flex-gap 的浏览器 */
.actions > * + * {
  margin-left: 4px;
}

.hidden-file {
  display: none;
}

.popover-center-ref {
  position: absolute;
  left: 50%;
  top: 0;
  width: 0;
  height: 0;
}

/* 通用按钮样式 */
.action-button {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  flex-shrink: 0;
  position: relative;
  font-size: 16px;
  margin-right: 6px;

}

/* 参数按钮 */
.parameters-btn {
  background: transparent;
  color: #6b7280;
  border: 1.5px solid #e5e7eb;

}

.parameters-btn:hover:not(:disabled) {
  background: rgba(59, 130, 246, 0.08);
  color: #3b82f6;
  border-color: #3b82f6;
  transform: scale(1.05);
}

.parameters-btn.active {
  background: rgba(59, 130, 246, 0.12);
  color: #3b82f6;
  border-color: #3b82f6;
}

.parameters-btn.alert {
  color: #ef4444;
  border-color: #fecaca;
  background: rgba(239, 68, 68, 0.08);
}

.parameters-btn.alert:hover:not(:disabled) {
  background: rgba(239, 68, 68, 0.12);
  color: #dc2626;
  border-color: #fca5a5;
}


/* 文件上传按钮 */
.file-upload-btn {
  /* 稍微柔和一点的浅蓝色按钮，既醒目又不刺眼 */
  background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
  color: #ffffff;
  border: 1.5px solid #3b82f6;
}

.file-upload-btn:hover:not(:disabled) {
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  color: #ffffff;
  border-color: #2563eb;
  transform: scale(1.05);
}

/* 发送按钮 */
.send-button {
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  color: #ffffff;
  border: none;
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}

.send-button:hover:not(:disabled) {
  transform: scale(1.05);
  box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
  background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
}

.send-button:active:not(:disabled) {
  transform: scale(0.98);
}

.send-button:disabled {
  background: #9ca3af;
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}

/* 停止按钮 */
.stop-button {
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  color: #ffffff;
  border: none;
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
  outline: none !important; /* 移除焦点轮廓 */
}

.stop-button:hover {
  transform: scale(1.05);
  box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
  background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
}

.stop-button:active {
  transform: scale(0.98);
}

/* 移除所有状态下的边框和轮廓 */
.stop-button:focus,
.stop-button:active,
.stop-button:hover {
  outline: none !important;
  border: none !important;
  box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4); /* 保持悬停效果 */
}

/* 禁用状态 */
.action-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none !important;
  box-shadow: none !important;
}

/* 徽章样式 */
.badge {
  position: absolute;
  top: -5px;
  right: -5px;
  background: linear-gradient(135deg, #f87171 0%, #ef4444 50%, #dc2626 100%);
  color: #ffffff;
  border-radius: 12px;
  height: 22px;
  min-width: 22px;
  line-height: 22px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  padding: 0 5px;
  border: 2px solid #ffffff;
  box-shadow: 0 3px 8px rgba(239, 68, 68, 0.35), 0 1px 3px rgba(0, 0, 0, 0.15);
  -webkit-box-shadow: 0 3px 8px rgba(239, 68, 68, 0.35), 0 1px 3px rgba(0, 0, 0, 0.15);
  -moz-box-shadow: 0 3px 8px rgba(239, 68, 68, 0.35), 0 1px 3px rgba(0, 0, 0, 0.15);
  z-index: 100;
  letter-spacing: -0.3px;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .input-area {
    padding: 16px 20px;
  }

  .composer {
    padding: 14px 16px;
    border-radius: 14px;
  }

  .action-button {
    width: 36px;
    height: 36px;
    font-size: 15px;

  }

  .input {
    font-size: 14px;
  }

  .attachment-card {
    max-width: 200px;
    min-width: 160px;
  }

  .attachments-header {
    padding: 6px 10px;
  }
}

@media (max-width: 480px) {
  .input-area {
    padding: 12px 16px;
  }

  .composer {
    padding: 12px 14px;
    border-radius: 12px;
  }

  .action-button {
    width: 32px;
    height: 32px;
    font-size: 14px;
  }

  .input-row {
    gap: 8px;
  }

  .attachment-card {
    max-width: 100%;
    min-width: auto;
  }
}

.file-icon {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  background: #eff4ff;
  color: #3b82f6;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  flex-shrink: 0;
}

.file-meta {
  flex: 1;
  overflow: hidden;
}

.file-name {
  font-size: 12px;
  color: #374151;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-size {
  font-size: 11px;
  color: #9ca3af;
}

.att-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid #ffffff;
  background: #ef4444;
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 12px;
}

.att-remove:hover {
  background: #dc2626;
}

.input-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

.input {
  flex: 1;
  min-height: 24px;
  max-height: 120px;
  border: none;
  outline: none;
  font-size: 14px;
  color: #111827;
  background: transparent;
  resize: none;
  font-family: inherit;
  line-height: 1.5;
}

.input::placeholder {
  color: #9ca3af;
}

.actions {
  display: flex;
  align-items: center;

}

.hidden-file {
  display: none;
}

.popover-center-ref {
  position: absolute;
  left: 50%;
  top: 0;
  width: 0;
  height: 0;
}

.attach,
.send,
.stop {
  width: 36px;
  height: 36px;
  border-radius: 12px;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  flex-shrink: 0;
}

.attach {
  background: transparent;
  color: #9ca3af;
}

.attach:hover:not(:disabled) {
  background: rgba(59, 130, 246, 0.08);
  color: var(--c-law-blue);
  transform: scale(1.05);
}

.send {
  background: linear-gradient(
      135deg,
      var(--c-primary) 0%,
      var(--c-primary-2) 100%
  );
  color: #ffffff;
  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.25);
}

.send:hover:not(:disabled) {
  transform: scale(1.05);
  box-shadow: 0 6px 24px rgba(59, 130, 246, 0.35);
}

.send:active:not(:disabled) {
  transform: scale(0.95);
}

.stop {
  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  color: #ffffff;
  box-shadow: 0 4px 16px rgba(239, 68, 68, 0.25);
}

.stop:hover {
  transform: scale(1.05);
  box-shadow: 0 6px 24px rgba(239, 68, 68, 0.35);
}

.stop:active {
  transform: scale(0.95);
}

.attach:disabled,
.send:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none !important;
}

.attach i,
.send i,
.stop i {
  font-size: 16px;
}

/* 下一步问题建议（消息底部）*/
.suggested-questions-container {
  margin: 16px auto;
  padding: 14px 18px;
  background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  animation: fadeIn 0.3s ease;
  box-shadow: 0 6px 20px rgba(17, 24, 39, 0.06);
  max-width: 900px;
  width: calc(100% - 48px);
  box-sizing: border-box;
}

/* 响应式：超大屏幕 */
@media (min-width: 1400px) {
  .suggested-questions-container {
    max-width: 1000px;
  }
}

/* 响应式：中等屏幕 */
@media (max-width: 768px) {
  .suggested-questions-container {
    width: calc(100% - 32px);
    margin: 16px auto;
  }
}

.suggested-questions-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  color: #6b7280;
  font-size: 13px;
  font-weight: 600;
}

.suggested-questions-title i {
  font-size: 16px;
  color: #3b82f6;
}

.suggested-questions-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.suggested-question-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
}

.suggested-question-chip:hover {
  background: #f3f4f6;
  border-color: #3b82f6;
  transform: translateX(4px);
}

.suggested-question-chip span {
  flex: 1;
  font-size: 14px;
  color: #374151;
  line-height: 1.5;
}

.suggested-question-chip i {
  font-size: 14px;
  color: #9ca3af;
  transition: color 0.2s;
}

.suggested-question-chip:hover i {
  color: #3b82f6;
}

/* 文件链接（旧样式兼容）*/
.message-text ::v-deep .file-link-wrapper {
  display: -webkit-inline-box;
  display: -ms-inline-flexbox;
  display: inline-flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  /* gap: 8px; */ /* Chrome 84+ 才支持，改用 margin 替代 */
  padding: 8px 12px;
  background: #eff6ff;
  border: 1px solid #dbeafe;
  border-radius: 6px;
  margin: 4px 0;
  -webkit-transition: all 0.2s;
  -o-transition: all 0.2s;
  transition: all 0.2s;
}

/* 使用 margin-left 替代 gap（兼容 Chrome 75+）*/
.message-text ::v-deep .file-link-wrapper > * + * {
  margin-left: 8px;
}

.message-text ::v-deep .file-link-wrapper:hover {
  background: #dbeafe;
  border-color: var(--c-primary);
  -webkit-transform: translateX(2px);
  -ms-transform: translateX(2px);
  transform: translateX(2px);
}

.message-text ::v-deep .file-link-wrapper i {
  font-size: 18px;
  color: #3b82f6;
}

.message-text ::v-deep .file-link {
  color: #374151;
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  -webkit-transition: color 0.2s;
  -o-transition: color 0.2s;
  transition: color 0.2s;
}

.message-text ::v-deep .file-link:hover {
  color: var(--c-primary);
  text-decoration: underline;
}

/* 文件卡片样式 - 美观且兼容 Chrome 75+ */
.message-text ::v-deep .file-card {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  /* gap: 12px; */ /* Chrome 84+ 才支持，改用 margin 替代 */
  padding: 12px 16px;
  margin: 8px 0;
  background: -webkit-gradient(linear, left top, right bottom, from(#f6f8fb), to(#ffffff));
  background: -o-linear-gradient(top left, #f6f8fb, #ffffff);
  background: linear-gradient(135deg, #f6f8fb 0%, #ffffff 100%);
  border: 1.5px solid #e1e8f0;
  border-radius: 10px;
  -webkit-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  -o-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

/* 使用 margin-left 替代 gap（兼容 Chrome 75+）*/
.message-text ::v-deep .file-card > * + * {
  margin-left: 12px;
}

.message-text ::v-deep .file-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 3px;
  background: -webkit-gradient(linear, left top, left bottom, from(#3b82f6), to(#2563eb));
  background: -o-linear-gradient(top, #3b82f6, #2563eb);
  background: linear-gradient(180deg, #3b82f6, #2563eb);
  opacity: 0;
  -webkit-transition: opacity 0.3s;
  -o-transition: opacity 0.3s;
  transition: opacity 0.3s;
}

.message-text ::v-deep .file-card:hover {
  -webkit-transform: translateY(-2px);
  -ms-transform: translateY(-2px);
  transform: translateY(-2px);
  -webkit-box-shadow: 0 8px 16px rgba(59, 130, 246, 0.15);
  box-shadow: 0 8px 16px rgba(59, 130, 246, 0.15);
  border-color: #3b82f6;
}

.message-text ::v-deep .file-card:hover::before {
  opacity: 1;
}

/* 文件图标区域 */
.message-text ::v-deep .file-icon {
  position: relative;
  width: 48px;
  height: 48px;
  -webkit-flex-shrink: 0;
  -ms-flex-negative: 0;
  flex-shrink: 0;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  background: -webkit-gradient(linear, left top, right bottom, from(#eff6ff), to(#dbeafe));
  background: -o-linear-gradient(top left, #eff6ff, #dbeafe);
  background: linear-gradient(135deg, #eff6ff, #dbeafe);
  border-radius: 8px;
  -webkit-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
}

.message-text ::v-deep .file-card:hover .file-icon {
  background: -webkit-gradient(linear, left top, right bottom, from(#dbeafe), to(#bfdbfe));
  background: -o-linear-gradient(top left, #dbeafe, #bfdbfe);
  background: linear-gradient(135deg, #dbeafe, #bfdbfe);
  -webkit-transform: scale(1.05);
  -ms-transform: scale(1.05);
  transform: scale(1.05);
}

.message-text ::v-deep .file-icon i {
  font-size: 24px;
  color: #3b82f6;
  -webkit-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
}

.message-text ::v-deep .file-card:hover .file-icon i {
  color: #2563eb;
  -webkit-transform: scale(1.1);
  -ms-transform: scale(1.1);
  transform: scale(1.1);
}

.message-text ::v-deep .file-ext {
  position: absolute;
  bottom: 2px;
  right: 2px;
  font-size: 8px;
  font-weight: 700;
  color: #1e40af;
  background: rgba(255, 255, 255, 0.95);
  padding: 1px 3px;
  border-radius: 2px;
  line-height: 1;
  letter-spacing: 0.3px;
}

/* 文件信息区域 */
.message-text ::v-deep .file-info {
  -webkit-box-flex: 1;
  -ms-flex: 1;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

.message-text ::v-deep .file-name {
  font-size: 14px;
  font-weight: 500;
  color: #1f2937;
  margin-bottom: 4px;
  overflow: hidden;
  -o-text-overflow: ellipsis;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.4;
}

.message-text ::v-deep .file-size {
  font-size: 12px;
  color: #6b7280;
  line-height: 1.2;
}

/* 下载按钮 */
.message-text ::v-deep .file-download {
  width: 36px;
  height: 36px;
  -webkit-flex-shrink: 0;
  -ms-flex-negative: 0;
  flex-shrink: 0;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  background: -webkit-gradient(linear, left top, right bottom, from(#3b82f6), to(#2563eb));
  background: -o-linear-gradient(top left, #3b82f6, #2563eb);
  background: linear-gradient(135deg, #3b82f6, #2563eb);
  color: #ffffff;
  border-radius: 8px;
  text-decoration: none;
  -webkit-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
  -webkit-box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
  box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
}

.message-text ::v-deep .file-download i {
  font-size: 16px;
  font-weight: bold;
}

.message-text ::v-deep .file-download:hover {
  background: -webkit-gradient(linear, left top, right bottom, from(#2563eb), to(#1d4ed8));
  background: -o-linear-gradient(top left, #2563eb, #1d4ed8);
  background: linear-gradient(135deg, #2563eb, #1d4ed8);
  -webkit-transform: translateY(-1px) scale(1.05);
  -ms-transform: translateY(-1px) scale(1.05);
  transform: translateY(-1px) scale(1.05);
  -webkit-box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

.message-text ::v-deep .file-download:active {
  -webkit-transform: translateY(0) scale(0.98);
  -ms-transform: translateY(0) scale(0.98);
  transform: translateY(0) scale(0.98);
}

@supports (
  (-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))
) {
  .sidebar {
    background: rgba(255, 255, 255, 0.6);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }

  .composer {
    background: rgba(255, 255, 255, 0.72);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }

  .message-wrapper.assistant .message-text {
    background: rgba(255, 255, 255, 0.9);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
  }
}

/* 引用和归属 */
.message-text ::v-deep .references-section {
  margin-top: 16px;
  padding: 16px;
  background: #f9fafb;
  border-left: 3px solid #3b82f6;
  border-radius: 8px;
}

.message-text ::v-deep .references-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #374151;
}

.message-text ::v-deep .references-title i {
  font-size: 16px;
  color: #3b82f6;
}

.message-text ::v-deep .reference-item {
  margin-top: 12px;
  padding: 12px;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
}

.message-text ::v-deep .reference-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.message-text ::v-deep .reference-index {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  background: #3b82f6;
  color: #ffffff;
  font-size: 12px;
  font-weight: 600;
  border-radius: 50%;
}

.message-text ::v-deep .reference-dataset {
  font-size: 13px;
  font-weight: 600;
  color: #374151;
}

.message-text ::v-deep .reference-score {
  margin-left: auto;
  font-size: 12px;
  color: #6b7280;
  background: #f3f4f6;
  padding: 2px 8px;
  border-radius: 4px;
}

.message-text ::v-deep .reference-content {
  font-size: 13px;
  color: #6b7280;
  line-height: 1.6;
  font-style: italic;
}

/* 用户消息内容容器 */
.user-message-content {
  width: 100%;
}

.user-message-text {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -ms-flex-direction: column;
  flex-direction: column;
}

/* Chrome 75-77 兼容：用 margin-top 替代 gap */
.user-message-text > * + * {
  margin-top: 8px;
}

.user-message-text-body {
  width: 100%;
  color: inherit;
}

/* 用户消息框体内的文件显示 - 使用文件卡片样式 */
.user-files-cards {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -ms-flex-direction: column;
  flex-direction: column;
  margin-bottom: 8px;
}

.user-files-cards > * + * {
  margin-top: 8px; /* 卡片间距，兼容旧浏览器 */
}

/* 用户文件卡片特殊样式（可选，用于区分用户和AI的文件） */
.user-file-card {
  background: -webkit-gradient(linear, left top, right bottom, from(#fafbfc), to(#ffffff));
  background: -o-linear-gradient(top left, #fafbfc, #ffffff);
  background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%);
  border: 1.5px solid #e5e7eb;
}

.user-file-card:hover {
  border-color: #93c5fd;
  -webkit-box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
  -webkit-transform: translateX(-2px);
  -ms-transform: translateX(-2px);
  transform: translateX(-2px);
}

/* 用户文件卡片的下载按钮颜色 */
.user-file-card .file-download {
  background: -webkit-gradient(linear, left top, right bottom, from(#3b82f6), to(#2563eb));
  background: -o-linear-gradient(top left, #3b82f6, #2563eb);
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}

.user-file-card .file-download:hover {
  background: -webkit-gradient(linear, left top, right bottom, from(#2563eb), to(#1d4ed8));
  background: -o-linear-gradient(top left, #2563eb, #1d4ed8);
  background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
  -webkit-box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
  box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
}

/* 预览按钮样式（当文件没有URL时） */
.file-preview {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  background: -webkit-gradient(linear, left top, right bottom, from(#6b7280), to(#4b5563));
  background: -o-linear-gradient(top left, #6b7280, #4b5563);
  background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
  border-radius: 8px;
  cursor: pointer;
  -webkit-transition: all 0.3s ease;
  -o-transition: all 0.3s ease;
  transition: all 0.3s ease;
  -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  text-decoration: none;
  -ms-flex-negative: 0;
  flex-shrink: 0;
}

.file-preview:hover {
  background: -webkit-gradient(linear, left top, right bottom, from(#4b5563), to(#374151));
  background: -o-linear-gradient(top left, #4b5563, #374151);
  background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
  -webkit-box-shadow: 0 4px 12px rgba(75, 85, 99, 0.4);
  box-shadow: 0 4px 12px rgba(75, 85, 99, 0.4);
  -webkit-transform: translateY(-2px) scale(1.05);
  -ms-transform: translateY(-2px) scale(1.05);
  transform: translateY(-2px) scale(1.05);
}

.file-preview i {
  color: white;
  font-size: 18px;
  font-weight: bold;
}


/* 代码消息样式 */
.code-message-wrapper {
  width: 100%;
  background: #f8fafc;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  border: 1px solid #e2e8f0;
}

.code-block-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  background: #f1f5f9;
  border-bottom: 1px solid #e2e8f0;
}

.code-language {
  font-size: var(--font-size-text-small);
  font-weight: var(--font-weight-semibold);
  color: #475569;
  font-family: var(--font-family-code);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.code-actions {
  display: flex;
  gap: 8px;
}

.code-action-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 14px;
  background: #ffffff;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  color: #64748b;
  font-size: var(--font-size-text-tiny);
  cursor: pointer;
  transition: all 0.2s;
}

.code-action-btn:hover {
  background: #3b82f6;
  border-color: #3b82f6;
  color: #ffffff;
  transform: translateY(-1px);
  box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
}

.code-action-btn i {
  font-size: 13px;
}

.code-block-content {
  padding: 32px;
  background: #f8fafc;
  overflow-x: auto;
  max-height: 500px;
  overflow-y: auto;

  /* 自定义滚动条 */
  &::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  &::-webkit-scrollbar-track {
    background: #e2e8f0;
    border-radius: 4px;
  }

  &::-webkit-scrollbar-thumb {
    background: #94a3b8;
    border-radius: 4px;
  }

  &::-webkit-scrollbar-thumb:hover {
    background: #64748b;
  }
}

.code-preview {
  margin: 0;
  padding: 0;
  background: transparent;
  color: #334155;
  font-family: var(--font-family-code);
  font-size: var(--font-size-text-normal);
  line-height: var(--line-height-relaxed);
}

.code-preview code {
  display: block;
  white-space: pre;
  background: transparent;
  color: #334155;
}

.code-message-wrapper .message-actions {
  padding: 12px 20px;
  background: #f8fafc;
  border-top: 1px solid #e2e8f0;
}

.code-message-wrapper .message-time {
  color: #64748b;
}

/* 加载动画 */
.typing-indicator {
  display: flex;
  /*gap: 6px;*/
  padding: 16px 20px;
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 16px;
  border-bottom-left-radius: 4px;
  width: fit-content;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);

}

.typing-indicator span {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  animation: typing 1.4s infinite ease-in-out;
  margin-right: 6px;
}

.typing-indicator span:nth-child(2) {
  animation-delay: 0.2s;
}

.typing-indicator span:nth-child(3) {
  animation-delay: 0.4s;
}

/* 内联加载动画（在消息内容中显示） */
.typing-indicator-inline {
  display: flex;
  gap: 6px;
  padding: 12px 0;
  align-items: center;
}

.typing-indicator-inline span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #9ca3af;
  animation: typing 1.4s infinite ease-in-out;
}

.typing-indicator-inline span:nth-child(2) {
  animation-delay: 0.2s;
}

.typing-indicator-inline span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes typing {
  0%,
  60%,
  100% {
    opacity: 0.3;
    transform: scale(0.7);
  }
  30% {
    opacity: 1;
    transform: scale(1.1);
  }
}

/* 页面禁用遮罩样式 */
.page-disabled-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
}
.mask-content {
  background: #fff;
  border-radius: 8px;
  padding: 24px 28px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  text-align: center;
  max-width: 520px;
}
.mask-icon {
  font-size: 32px;
  color: #e6a23c;
}
.mask-title {
  margin-top: 8px;
  font-size: 18px;
  font-weight: 600;
  color: #303133;
}
.mask-desc {
  margin-top: 6px;
  color: #606266;
}
</style>

<!-- 全局样式：Dify 文件类型提示弹窗 -->
<style lang="scss">
.dify-file-type-alert {
  width: 480px;
  max-width: 92vw;

  .el-message-box__message {
    max-height: none;
  }
}
</style>
